291 lines
8.9 KiB
TypeScript
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();
|
|
});
|
|
});
|