Files
shd.build/src/routes/builder/skill-chip-save.test.ts
T
jb 4777a3abd5
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 44s
Validate and deploy staging / deploy (push) Successful in 1m14s
feat: refine builder skill chip flows
2026-08-06 01:00:15 -04:00

291 lines
8.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createDraft, type LoadoutDraft } from '$lib/builder/loadout';
const mocks = vi.hoisted(() => ({
insertBuild: vi.fn(),
getSessionUser: vi.fn(async () => ({
id: 'agent-1',
displayName: 'Test Agent',
})),
database: {
prepare: vi.fn(),
},
}));
vi.mock('$lib/server/auth/session', () => ({
getSessionUser: mocks.getSessionUser,
}));
vi.mock('$lib/server/database', () => ({
getDatabase: vi.fn(async () => mocks.database),
}));
import { actions } from './+page.server';
const primaryStatId = 'skill-duration';
const secondaryStatIds = ['critical-hit-chance', 'critical-hit-damage'];
const validLoadout = (): LoadoutDraft => {
const loadout = createDraft();
loadout.buildName = 'Validated Skill Chips';
loadout.specialization.focusId = 'focus-1';
loadout.chips[0] = {
setId: 'chip-set-1',
quality: 'Superior',
primary: { statId: primaryStatId, value: 15 },
secondary: [
{ statId: secondaryStatIds[0], value: 2 },
{ statId: secondaryStatIds[1], value: 4 },
],
};
return loadout;
};
const saveRequest = (loadout: LoadoutDraft) => {
const form = new FormData();
form.set('loadout', JSON.stringify(loadout));
form.set('visibility', 'private');
return new Request('http://localhost/builder?/save', {
method: 'POST',
body: form,
});
};
const runSave = (loadout: LoadoutDraft) =>
actions.save?.({
cookies: {} as never,
request: saveRequest(loadout),
} as never);
describe('Builder Skill Chip save validation', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.database.prepare.mockImplementation(async (sql: string) => ({
all: vi.fn(async () => {
if (sql.includes('FROM skill_chip_sets'))
return [
{
id: 'chip-set-1',
specialization_ability_id: 'ability-1',
},
];
if (sql.includes('FROM specialization_focus_abilities'))
return [{ ability_id: 'ability-1', ability_slot: 1 }];
if (sql.includes('FROM skill_chip_primary_options'))
return [{ stat_attribute_id: primaryStatId }];
if (sql.includes('FROM skill_chip_primary_quality_values'))
return [
{
stat_attribute_id: primaryStatId,
quality: 'Superior',
min_value: 10,
max_value: 20,
},
];
if (sql.includes('FROM skill_chip_secondary_options'))
return secondaryStatIds.map((stat_attribute_id) => ({
stat_attribute_id,
}));
if (sql.includes('FROM skill_chip_secondary_quality_values'))
return secondaryStatIds.map((stat_attribute_id) => ({
stat_attribute_id,
quality: 'Superior',
min_value: 1,
max_value: 5,
}));
return [];
}),
run: mocks.insertBuild,
}));
});
it('rejects a selected Skill Chip assigned to a different focus ability', async () => {
mocks.database.prepare.mockImplementation(async (sql: string) => ({
all: vi.fn(async () => {
if (sql.includes('FROM skill_chip_sets'))
return [
{
id: 'chip-set-1',
specialization_ability_id: 'ability-2',
},
];
if (sql.includes('FROM specialization_focus_abilities'))
return [{ ability_id: 'ability-1', ability_slot: 1 }];
return [];
}),
run: mocks.insertBuild,
}));
const result = await runSave(validLoadout());
expect(result).toMatchObject({
status: 400,
data: {
buildError: 'Skill Chip 1 is not compatible with its assigned ability.',
},
});
expect(mocks.insertBuild).not.toHaveBeenCalled();
});
it('accepts a Skill Chip owned by an explicitly compatible ability', async () => {
mocks.database.prepare.mockImplementation(async (sql: string) => ({
all: vi.fn(async () => {
if (sql.includes('FROM skill_chip_sets'))
return [
{
id: 'chip-set-1',
specialization_ability_id: 'ability-2',
},
];
if (sql.includes('FROM specialization_ability_skill_chip_links'))
return [
{
ability_id: 'ability-1',
compatible_ability_id: 'ability-2',
},
];
if (sql.includes('FROM specialization_focus_abilities'))
return [{ ability_id: 'ability-1', ability_slot: 1 }];
if (sql.includes('FROM skill_chip_primary_options'))
return [{ stat_attribute_id: primaryStatId }];
if (sql.includes('FROM skill_chip_primary_quality_values'))
return [
{
stat_attribute_id: primaryStatId,
quality: 'Superior',
min_value: 10,
max_value: 20,
},
];
if (sql.includes('FROM skill_chip_secondary_options'))
return secondaryStatIds.map((stat_attribute_id) => ({
stat_attribute_id,
}));
if (sql.includes('FROM skill_chip_secondary_quality_values'))
return secondaryStatIds.map((stat_attribute_id) => ({
stat_attribute_id,
quality: 'Superior',
min_value: 1,
max_value: 5,
}));
return [];
}),
run: mocks.insertBuild,
}));
const result = await runSave(validLoadout());
expect(result).toMatchObject({ buildSaved: true });
expect(mocks.insertBuild).toHaveBeenCalledOnce();
});
it.each([
{
label: 'primary',
mutate: (loadout: LoadoutDraft) => {
loadout.chips[0].primary.value = 21;
},
message: 'Skill Chip 1 has an invalid primary Attribute value.',
},
{
label: 'secondary',
mutate: (loadout: LoadoutDraft) => {
loadout.chips[0].secondary[1].value = 6;
},
message: 'Skill Chip 1 has an invalid secondary Attribute value.',
},
])('rejects an out-of-range $label value', async ({ mutate, message }) => {
const loadout = validLoadout();
mutate(loadout);
const result = await runSave(loadout);
expect(result).toMatchObject({
status: 400,
data: { buildError: message },
});
expect(mocks.insertBuild).not.toHaveBeenCalled();
});
it('rejects duplicate secondary Attributes', async () => {
const loadout = validLoadout();
loadout.chips[0].secondary[1].statId = secondaryStatIds[0];
const result = await runSave(loadout);
expect(result).toMatchObject({
status: 400,
data: {
buildError: 'Skill Chip 1 must use distinct secondary Attributes.',
},
});
expect(mocks.insertBuild).not.toHaveBeenCalled();
});
it('accepts primary and secondary values within the configured profiles', async () => {
const result = await runSave(validLoadout());
expect(result).toMatchObject({
buildSaved: true,
buildToken: expect.any(String),
savedBuild: {
visibility: 'private',
title: 'Validated Skill Chips',
},
});
expect(mocks.insertBuild).toHaveBeenCalledOnce();
});
it('rejects an equipment Talent tier without canonical tier data', async () => {
const loadout = createDraft();
loadout.buildName = 'Invalid Talent Tier';
loadout.gear.Holster.itemId = 'gear-1';
loadout.gear.Holster.talentId = 'talent-1';
loadout.gear.Holster.talentTier = 2;
mocks.database.prepare.mockImplementation(async (sql: string) => ({
all: vi.fn(async () => {
if (sql.includes('FROM gear_talents'))
return [{ gear_id: 'gear-1', talent_id: 'talent-1' }];
if (sql.includes('FROM talent_tiers'))
return [{ talent_id: 'talent-1', tier: 'I' }];
return [];
}),
run: mocks.insertBuild,
}));
const result = await runSave(loadout);
expect(result).toMatchObject({
status: 400,
data: { buildError: 'Holster Talent T2 is not configured.' },
});
expect(mocks.insertBuild).not.toHaveBeenCalled();
});
it('saves an equipment Talent with an authored Tier II', async () => {
const loadout = createDraft();
loadout.buildName = 'Tier Two Talent';
loadout.gear.Holster.itemId = 'gear-1';
loadout.gear.Holster.talentId = 'talent-1';
loadout.gear.Holster.talentTier = 2;
mocks.database.prepare.mockImplementation(async (sql: string) => ({
all: vi.fn(async () => {
if (sql.includes('FROM gear_talents'))
return [{ gear_id: 'gear-1', talent_id: 'talent-1' }];
if (sql.includes('FROM talent_tiers'))
return [
{ talent_id: 'talent-1', tier: 'I' },
{ talent_id: 'talent-1', tier: 'II' },
];
return [];
}),
run: mocks.insertBuild,
}));
const result = await runSave(loadout);
expect(result).toMatchObject({ buildSaved: true });
expect(mocks.insertBuild).toHaveBeenCalledOnce();
});
});