feat(toolkit): add structured media authoring
This commit is contained in:
@@ -10,8 +10,8 @@ describe('changelog data', () => {
|
||||
expect(changelogEntries.length).toBeGreaterThan(0);
|
||||
expect(changelogEntries).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'build-metadata-changelog-poc',
|
||||
title: 'Build Metadata and Changelog Scaffold',
|
||||
id: 'consolidated-toolkit-content-files',
|
||||
title: 'Consolidated Toolkit Content Files',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { fallbackCreditsContent, isCreditsContent } from '../src/data';
|
||||
import {
|
||||
fallbackCreditsContent,
|
||||
isCreditsContent,
|
||||
isLegalContent,
|
||||
legalContent,
|
||||
toolkitData,
|
||||
} from '../src/data';
|
||||
|
||||
describe('credits data', () => {
|
||||
it('provides valid fallback credits content', () => {
|
||||
@@ -21,4 +27,14 @@ describe('credits data', () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('exports legal and toolkit data through the root data index', () => {
|
||||
expect(isLegalContent(legalContent)).toBe(true);
|
||||
expect(toolkitData.petitioners.length).toBeGreaterThan(0);
|
||||
expect(toolkitData.rulings.length).toBeGreaterThan(0);
|
||||
expect(toolkitData.map.settlements.length).toBeGreaterThan(0);
|
||||
expect(toolkitData.credits.mainLogo.title).toBe('Throne of the Bone King');
|
||||
expect(toolkitData.changelog.length).toBeGreaterThan(0);
|
||||
expect(toolkitData.legal.pages.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('routeDefinitions', () => {
|
||||
'toolkitFlags',
|
||||
'toolkitStory',
|
||||
'toolkitLore',
|
||||
'toolkitImages',
|
||||
'toolkitAudio',
|
||||
'toolkitCredits',
|
||||
'toolkitEndings',
|
||||
@@ -73,6 +74,7 @@ describe('routeDefinitions', () => {
|
||||
'toolkitFlags',
|
||||
'toolkitStory',
|
||||
'toolkitLore',
|
||||
'toolkitImages',
|
||||
'toolkitAudio',
|
||||
'toolkitCredits',
|
||||
'toolkitEndings',
|
||||
|
||||
@@ -266,6 +266,8 @@ describe('screen scaffold', () => {
|
||||
const renderer = render(<ChangelogScreen goHome={jest.fn()} />);
|
||||
const text = getRenderedText(renderer);
|
||||
|
||||
expect(text).toContain('Changelog Discipline Guidance');
|
||||
expect(text).toContain('meaningful commit and push work');
|
||||
expect(text).toContain('Build Metadata and Changelog Scaffold');
|
||||
expect(text).toContain('0.0.1-20260616-13aa686');
|
||||
expect(text).toContain('Seeded data-backed changelog content');
|
||||
@@ -427,7 +429,6 @@ describe('screen scaffold', () => {
|
||||
);
|
||||
const text = getRenderedText(renderer);
|
||||
|
||||
expect(text).toContain('Toolkit');
|
||||
expect(text).toContain('Opening');
|
||||
expect(text).toContain('Petitioners');
|
||||
expect(text).toContain('Rulings');
|
||||
@@ -438,12 +439,11 @@ describe('screen scaffold', () => {
|
||||
expect(text).toContain('Flags');
|
||||
expect(text).toContain('Story');
|
||||
expect(text).toContain('Lore');
|
||||
expect(text).toContain('Images');
|
||||
expect(text).toContain('Audio');
|
||||
expect(text).toContain('Credits');
|
||||
expect(text).toContain('Endings');
|
||||
expect(text).toContain('References');
|
||||
expect(text).toContain('Read-only planning panel');
|
||||
expect(text).toContain('Future authoring area');
|
||||
|
||||
const homeButton = renderer.root.findByProps({ testID: 'toolkit-home' });
|
||||
|
||||
@@ -455,23 +455,7 @@ describe('screen scaffold', () => {
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-socket-indicator' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders purpose panels for every toolkit section without forms', () => {
|
||||
const expectedPurposeText = {
|
||||
toolkitAudio: 'Preview and catalog sound identity',
|
||||
toolkitEndings: 'Tune collapse states',
|
||||
toolkitFactions: 'Tune faction trust',
|
||||
toolkitFlags: 'Debug delayed consequences',
|
||||
toolkitLore: 'Reference changing canon',
|
||||
toolkitMap: 'Support settlement and world-map iteration',
|
||||
toolkitOpening: 'Preview and sequence the first-run premise',
|
||||
toolkitPetitioners: 'Inspect and eventually author throne-room cases',
|
||||
toolkitReferences: 'Keep design inspirations',
|
||||
toolkitResources: 'Tune the core Bones, Minions, and Milk economy',
|
||||
toolkitRulings: 'Inspect and eventually tune choice outcomes',
|
||||
toolkitSeed: 'Debug why a day produced specific petitioners',
|
||||
toolkitStory: 'Organize Act I-IV pressure',
|
||||
} as const;
|
||||
|
||||
it('keeps legacy planning copy out of toolkit sections', () => {
|
||||
for (const routeName of toolkitRoutes) {
|
||||
if (routeName === 'toolkitCredits') {
|
||||
continue;
|
||||
@@ -486,21 +470,114 @@ describe('screen scaffold', () => {
|
||||
);
|
||||
const text = getRenderedText(renderer);
|
||||
|
||||
expect(text).toContain(expectedPurposeText[routeName]);
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-purpose' })).toBeTruthy();
|
||||
expect(
|
||||
renderer.root.findByProps({ testID: 'toolkit-change-note' }),
|
||||
).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-sources' })).toBeTruthy();
|
||||
renderer.root.findAllByProps({ testID: 'toolkit-purpose' }),
|
||||
).toHaveLength(0);
|
||||
expect(
|
||||
renderer.root.findByProps({ testID: 'toolkit-future-fields' }),
|
||||
).toBeTruthy();
|
||||
renderer.root.findAllByProps({ testID: 'toolkit-change-note' }),
|
||||
).toHaveLength(0);
|
||||
expect(renderer.root.findAllByProps({ testID: 'toolkit-sources' })).toHaveLength(0);
|
||||
expect(
|
||||
renderer.root.findByProps({ testID: 'toolkit-future-placeholder' }),
|
||||
).toBeTruthy();
|
||||
renderer.root.findAllByProps({ testID: 'toolkit-future-fields' }),
|
||||
).toHaveLength(0);
|
||||
expect(
|
||||
renderer.root.findAllByProps({ testID: 'toolkit-future-placeholder' }),
|
||||
).toHaveLength(0);
|
||||
expect(text).not.toContain('Future authoring area');
|
||||
expect(text).not.toContain('Source wiki pages');
|
||||
expect(text).not.toContain('Anticipated future fields / entities');
|
||||
expect(text).not.toContain('Structured authoring panel');
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-record-form' })).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders authoring controls for active toolkit resources', () => {
|
||||
const renderer = render(
|
||||
<ToolkitScreen
|
||||
goHome={jest.fn()}
|
||||
navigate={jest.fn()}
|
||||
routeName="toolkitPetitioners"
|
||||
/>,
|
||||
);
|
||||
const text = getRenderedText(renderer);
|
||||
|
||||
expect(text).not.toContain('Structured authoring panel');
|
||||
expect(text).not.toContain('Authoring');
|
||||
expect(text).not.toContain('Existing records');
|
||||
expect(text).toContain('Foreman Clatter');
|
||||
expect(text).toContain('Id');
|
||||
expect(text).toContain('Name');
|
||||
expect(text).toContain('Choices');
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-editor-petitioners' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-record-list' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-record-form' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-field-id-input' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-field-name-input' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-field-choices-add' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-editor-new' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-editor-refresh' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-editor-import' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-editor-update' })).toBeTruthy();
|
||||
expect(renderer.root.findAllByProps({ testID: 'toolkit-editor-create' })).toHaveLength(
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
renderer.root.findByProps({
|
||||
testID: 'toolkit-record-delete-petitioner_marrowgate_bad_bones_001',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(renderer.root.findAllByProps({ testID: 'toolkit-editor-delete' })).toHaveLength(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders simplified audio upload and playback controls', () => {
|
||||
const renderer = render(
|
||||
<ToolkitScreen
|
||||
goHome={jest.fn()}
|
||||
navigate={jest.fn()}
|
||||
routeName="toolkitAudio"
|
||||
/>,
|
||||
);
|
||||
const text = getRenderedText(renderer);
|
||||
|
||||
expect(text).toContain('Throne Room Theme');
|
||||
expect(text).toContain('Name');
|
||||
expect(text).toContain('Category');
|
||||
expect(text).toContain('Description');
|
||||
expect(text).toContain('File Name');
|
||||
expect(text).toContain('Audio Preview');
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-editor-upload-audio' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-audio-play-toggle' })).toBeTruthy();
|
||||
expect(renderer.root.findAllByProps({ testID: 'toolkit-field-id-input' })).toHaveLength(
|
||||
0,
|
||||
);
|
||||
expect(renderer.root.findAllByProps({ testID: 'toolkit-field-mood-input' })).toHaveLength(
|
||||
0,
|
||||
);
|
||||
expect(
|
||||
renderer.root.findAllByProps({ testID: 'toolkit-field-suggestedElements' }),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders image toolkit upload and preview controls', () => {
|
||||
const renderer = render(
|
||||
<ToolkitScreen
|
||||
goHome={jest.fn()}
|
||||
navigate={jest.fn()}
|
||||
routeName="toolkitImages"
|
||||
/>,
|
||||
);
|
||||
const text = getRenderedText(renderer);
|
||||
|
||||
expect(text).toContain('Images');
|
||||
expect(text).toContain('Main Menu Background');
|
||||
expect(text).toContain('Image Preview');
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-editor-images' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-editor-upload-image' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-image-preview' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the toolkit credits display card from fallback data', () => {
|
||||
const renderer = render(
|
||||
<ToolkitScreen
|
||||
@@ -518,7 +595,10 @@ describe('screen scaffold', () => {
|
||||
expect(text).toContain('Font Library contributors');
|
||||
expect(text).toContain(buildVersionLabel);
|
||||
expect(text).toContain('Bundled fallback credits');
|
||||
expect(text).not.toContain('Structured authoring panel');
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-credits-card' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-record-form' })).toBeTruthy();
|
||||
expect(renderer.root.findByProps({ testID: 'toolkit-field-mainLogo' })).toBeTruthy();
|
||||
expect(
|
||||
renderer.root.findByProps({ testID: 'toolkit-credits-contributors' }),
|
||||
).toBeTruthy();
|
||||
@@ -543,7 +623,7 @@ describe('screen scaffold', () => {
|
||||
const text = getRenderedText(renderer);
|
||||
const socketDot = renderer.root.findByProps({ testID: 'toolkit-socket-dot' });
|
||||
|
||||
expect(text).toContain('Connected');
|
||||
expect(text).not.toContain('Connected');
|
||||
expect(socketDot.props.style).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@@ -692,7 +772,9 @@ describe('screen scaffold', () => {
|
||||
|
||||
for (const routeName of toolkitRoutes) {
|
||||
pressByTestID(renderer, `toolkit-${routeName}`);
|
||||
expect(getRenderedText(renderer)).toContain(routeDefinitions[routeName].title);
|
||||
expect(
|
||||
renderer.root.findByProps({ testID: `toolkit-${routeName}` }),
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
pressByTestID(renderer, 'toolkit-home');
|
||||
|
||||
@@ -39,6 +39,15 @@ async function createTemporaryCreditsFile(content) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function createTemporaryJsonFile(content, prefix = 'blb-content-') {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||
const filePath = path.join(directory, 'content.json');
|
||||
|
||||
await fs.writeFile(filePath, `${JSON.stringify(content, null, 2)}\n`, 'utf8');
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
describe('socket content store', () => {
|
||||
it('reads valid JSON content with resource and revision metadata', async () => {
|
||||
const filePath = await createTemporaryCreditsFile(validCredits);
|
||||
@@ -94,4 +103,38 @@ describe('socket content store', () => {
|
||||
resource: 'credits',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates, updates, and deletes array-backed toolkit records', async () => {
|
||||
const filePath = await createTemporaryJsonFile([]);
|
||||
const store = createJsonContentStore({
|
||||
petitioners: filePath,
|
||||
});
|
||||
const record = {
|
||||
id: 'petitioner_test_001',
|
||||
name: 'Test Petitioner',
|
||||
};
|
||||
const updatedRecord = {
|
||||
...record,
|
||||
name: 'Updated Petitioner',
|
||||
};
|
||||
|
||||
await expect(
|
||||
store.createJsonRecord('petitioners', record),
|
||||
).resolves.toMatchObject({
|
||||
content: [record],
|
||||
resource: 'petitioners',
|
||||
});
|
||||
await expect(
|
||||
store.updateJsonRecord('petitioners', updatedRecord),
|
||||
).resolves.toMatchObject({
|
||||
content: [updatedRecord],
|
||||
resource: 'petitioners',
|
||||
});
|
||||
await expect(
|
||||
store.deleteJsonRecord('petitioners', updatedRecord.id),
|
||||
).resolves.toMatchObject({
|
||||
content: [],
|
||||
resource: 'petitioners',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,18 +2,27 @@ import { act } from 'react-test-renderer';
|
||||
|
||||
import { fallbackCreditsContent } from '../src/data';
|
||||
import {
|
||||
createToolkitContentRecord,
|
||||
deleteToolkitContentRecord,
|
||||
readToolkitContent,
|
||||
saveToolkitContent,
|
||||
updateToolkitContentRecord,
|
||||
} from '../src/socket/toolkitSocketClient';
|
||||
import { useToolkitContentStore } from '../src/state/toolkitContentStore';
|
||||
|
||||
jest.mock('../src/socket/toolkitSocketClient', () => ({
|
||||
createToolkitContentRecord: jest.fn(),
|
||||
deleteToolkitContentRecord: jest.fn(),
|
||||
readToolkitContent: jest.fn(),
|
||||
saveToolkitContent: jest.fn(),
|
||||
updateToolkitContentRecord: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedCreateToolkitContentRecord = jest.mocked(createToolkitContentRecord);
|
||||
const mockedDeleteToolkitContentRecord = jest.mocked(deleteToolkitContentRecord);
|
||||
const mockedReadToolkitContent = jest.mocked(readToolkitContent);
|
||||
const mockedSaveToolkitContent = jest.mocked(saveToolkitContent);
|
||||
const mockedUpdateToolkitContentRecord = jest.mocked(updateToolkitContentRecord);
|
||||
|
||||
const changedCredits = {
|
||||
...fallbackCreditsContent,
|
||||
@@ -28,12 +37,22 @@ describe('useToolkitContentStore', () => {
|
||||
beforeEach(() => {
|
||||
mockedReadToolkitContent.mockReset();
|
||||
mockedSaveToolkitContent.mockReset();
|
||||
mockedCreateToolkitContentRecord.mockReset();
|
||||
mockedDeleteToolkitContentRecord.mockReset();
|
||||
mockedUpdateToolkitContentRecord.mockReset();
|
||||
useToolkitContentStore.setState({
|
||||
contentByResource: {
|
||||
...useToolkitContentStore.getState().contentByResource,
|
||||
credits: fallbackCreditsContent,
|
||||
},
|
||||
credits: fallbackCreditsContent,
|
||||
error: null,
|
||||
errorsByResource: {},
|
||||
lastLoadedAt: null,
|
||||
revision: null,
|
||||
revisionsByResource: {},
|
||||
status: 'idle',
|
||||
statusByResource: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,4 +128,69 @@ describe('useToolkitContentStore', () => {
|
||||
status: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates, updates, and deletes toolkit records through the socket client', async () => {
|
||||
const newPetitioner = {
|
||||
id: 'petitioner_test_001',
|
||||
name: 'Test Petitioner',
|
||||
sourceWikiPages: ['Gameplay'],
|
||||
tags: ['test'],
|
||||
};
|
||||
const changedPetitioners = [newPetitioner];
|
||||
|
||||
mockedCreateToolkitContentRecord.mockResolvedValue({
|
||||
content: changedPetitioners,
|
||||
resource: 'petitioners',
|
||||
revision: '101',
|
||||
});
|
||||
mockedUpdateToolkitContentRecord.mockResolvedValue({
|
||||
content: changedPetitioners,
|
||||
resource: 'petitioners',
|
||||
revision: '102',
|
||||
});
|
||||
mockedDeleteToolkitContentRecord.mockResolvedValue({
|
||||
content: [],
|
||||
resource: 'petitioners',
|
||||
revision: '103',
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await useToolkitContentStore
|
||||
.getState()
|
||||
.createRecord('petitioners', newPetitioner);
|
||||
});
|
||||
|
||||
expect(mockedCreateToolkitContentRecord).toHaveBeenCalledWith(
|
||||
'petitioners',
|
||||
newPetitioner,
|
||||
);
|
||||
expect(useToolkitContentStore.getState().contentByResource.petitioners).toEqual(
|
||||
changedPetitioners,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await useToolkitContentStore
|
||||
.getState()
|
||||
.updateRecord('petitioners', newPetitioner);
|
||||
});
|
||||
|
||||
expect(mockedUpdateToolkitContentRecord).toHaveBeenCalledWith(
|
||||
'petitioners',
|
||||
newPetitioner,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await useToolkitContentStore
|
||||
.getState()
|
||||
.deleteRecord('petitioners', newPetitioner.id);
|
||||
});
|
||||
|
||||
expect(mockedDeleteToolkitContentRecord).toHaveBeenCalledWith(
|
||||
'petitioners',
|
||||
newPetitioner.id,
|
||||
);
|
||||
expect(useToolkitContentStore.getState().contentByResource.petitioners).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
const { readFileSync } = require('node:fs');
|
||||
const { existsSync, readFileSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const toolkitPlaceholderFiles = [
|
||||
const toolkitDataFiles = [
|
||||
'audio.json',
|
||||
'changelog.json',
|
||||
'credits.json',
|
||||
'endings.json',
|
||||
'factions.json',
|
||||
'flags.json',
|
||||
'images.json',
|
||||
'legal.json',
|
||||
'lore.json',
|
||||
'map.json',
|
||||
'opening.json',
|
||||
@@ -18,9 +21,20 @@ const toolkitPlaceholderFiles = [
|
||||
'story.json',
|
||||
];
|
||||
|
||||
describe('toolkit data placeholders', () => {
|
||||
it('provides an empty array JSON file for every toolkit section', () => {
|
||||
for (const fileName of toolkitPlaceholderFiles) {
|
||||
describe('toolkit data files', () => {
|
||||
it('keeps app content JSON consolidated under toolkit data', () => {
|
||||
for (const fileName of ['changelog.json', 'credits.json', 'legal.json']) {
|
||||
expect(
|
||||
existsSync(path.join(process.cwd(), 'src', 'data', 'toolkit', fileName)),
|
||||
).toBe(true);
|
||||
expect(existsSync(path.join(process.cwd(), 'src', 'data', fileName))).toBe(
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('provides starter JSON content for every toolkit data resource', () => {
|
||||
for (const fileName of toolkitDataFiles) {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
'src',
|
||||
@@ -30,7 +44,11 @@ describe('toolkit data placeholders', () => {
|
||||
);
|
||||
const content = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
|
||||
expect(content).toEqual([]);
|
||||
if (Array.isArray(content)) {
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
} else {
|
||||
expect(Object.keys(content).length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user