Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions src/components/dialogs/pf-paint-by-number-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export class PFPaintByNumberDialog extends BaseComponent {
@state() private guide: NumberedGuide | null = null;
@state() private longSide = 24;
@state() private maxColors = 8;
@state() private isAdvancedOpen = false;
@state() private paletteSource: GuidedPaletteSource = 'generated';
@state() private selectedPalette = GENERATED_PALETTE_VALUE;
@state() private restrictedPalette: string[] | undefined;
Expand Down Expand Up @@ -366,16 +367,23 @@ export class PFPaintByNumberDialog extends BaseComponent {
private renderGuidedSetup() {
return html`
<form id="guided-project-form" @submit=${this.createProject}>
<label class="file-field" for="guided-source-file">
<span class="field-label">Source image</span>
<div class="file-field">
<label class="field-label" for="guided-source-file">Source image</label>
<input
id="guided-source-file"
type="file"
accept="image/png,image/jpeg,image/webp"
@change=${this.handleFileChange}
>
${this.sourceFile
? html`
<span id="guided-source-filename" class="local-note">
Selected file: ${this.sourceFile.name}
</span>
`
: null}
<span class="local-note">Processed locally. The source image is not uploaded.</span>
</label>
</div>

<fieldset>
<legend>Guide preset</legend>
Expand All @@ -399,7 +407,7 @@ export class PFPaintByNumberDialog extends BaseComponent {
</div>
</fieldset>

<details>
<details .open=${this.isAdvancedOpen} @toggle=${this.handleAdvancedToggle}>
<summary>Advanced settings</summary>
<div class="advanced-fields">
<label class="field" for="guided-long-side">
Expand Down Expand Up @@ -699,6 +707,10 @@ export class PFPaintByNumberDialog extends BaseComponent {
this.schedulePreview();
};

private handleAdvancedToggle = (event: Event) => {
this.isAdvancedOpen = (event.currentTarget as HTMLDetailsElement).open;
};

private handlePaletteSelection = (event: Event) => {
const value = (event.currentTarget as HTMLSelectElement).value;
if (value === CREATE_PALETTE_VALUE) {
Expand Down
119 changes: 115 additions & 4 deletions tests/components/dialogs/pf-paint-by-number-dialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,65 @@ function chooseFile(element: PFPaintByNumberDialog, file: File) {
}

async function selectPalette(element: PFPaintByNumberDialog, value: string) {
const select = element.shadowRoot?.querySelector<HTMLSelectElement>(
'#guided-palette-source',
);
if (!select) throw new Error('Palette select is unavailable');
const select = getElement<HTMLSelectElement>(element, '#guided-palette-source');
select.value = value;
select.dispatchEvent(new Event('change'));
await settle(element);
}

function getElement<T extends Element>(
element: PFPaintByNumberDialog,
selector: string,
): T {
const match = element.shadowRoot?.querySelector<T>(selector);
if (!match) throw new Error(`Expected ${selector}`);
return match;
}

function getGuidedSetup(element: PFPaintByNumberDialog) {
return {
advanced: getElement<HTMLDetailsElement>(element, 'details'),
filename: getElement<HTMLElement>(element, '#guided-source-filename'),
guidePreview: getElement(element, '#guide-preview'),
longSide: getElement<HTMLInputElement>(element, '#guided-long-side'),
mapping: getElement<HTMLSelectElement>(element, '#guided-mapping'),
select: getElement<HTMLSelectElement>(element, '#guided-palette-source'),
simplify: getElement<HTMLInputElement>(element, '#guided-simplify'),
sourcePreview: getElement(element, '#source-preview'),
status: getElement(element, '#guided-project-form > .status'),
};
}

async function enterPaletteCreationWithSetup(
element: PFPaintByNumberDialog,
fileName: string,
) {
chooseFile(element, new File(['pixels'], fileName, { type: 'image/png' }));
await Promise.resolve();
await vi.runAllTimersAsync();

const advanced = getElement<HTMLDetailsElement>(element, 'details');
advanced.open = true;
advanced.dispatchEvent(new Event('toggle'));
await settle(element);

const longSide = getElement<HTMLInputElement>(element, '#guided-long-side');
longSide.value = '32';
longSide.dispatchEvent(new Event('change'));
const mapping = getElement<HTMLSelectElement>(element, '#guided-mapping');
mapping.value = 'luminance';
mapping.dispatchEvent(new Event('change'));
getElement<HTMLInputElement>(element, '#guided-simplify').click();
await selectPalette(element, 'preset:pico8');
await vi.runAllTimersAsync();
await settle(element);

const previewCalls = generateNumberedGuideMock.mock.calls.length;
const statusText = getElement(element, '#guided-project-form > .status').textContent;
await selectPalette(element, 'create');
return { previewCalls, statusText };
}

function customPalette(id: string, name: string, colors: string[]): CustomPalette {
return {
id,
Expand Down Expand Up @@ -232,6 +282,67 @@ describe('pf-paint-by-number-dialog', () => {
expect(status?.textContent).toContain(paletteError || paletteMessage);
});

it('restores visible setup context after cancelling palette creation', async () => {
const element = await createDialog();
expect(getElement<HTMLDetailsElement>(element, 'details').open).toBe(false);
const setup = await enterPaletteCreationWithSetup(element, 'cancel-source.png');

getElement<HTMLButtonElement>(
element,
'div[slot="actions"] button.secondary',
).click();
await settle(element);

const restored = getGuidedSetup(element);
expect(restored.filename.textContent).toContain('cancel-source.png');
expect(restored.advanced.open).toBe(true);
expect(restored.longSide.value).toBe('32');
expect(restored.mapping.value).toBe('luminance');
expect(restored.simplify.checked).toBe(false);
expect(restored.select.value).toBe('preset:pico8');
expect(restored.sourcePreview).toBeTruthy();
expect(restored.guidePreview).toBeTruthy();
expect(restored.status.textContent).toBe(setup.statusText);
expect(generateNumberedGuideMock).toHaveBeenCalledTimes(setup.previewCalls);
expect(element.shadowRoot?.activeElement).toBe(restored.select);
});

it('restores visible setup context after saving a palette', async () => {
const created = customPalette('new-palette', 'Saved palette', ['#123456']);
saveAsNewPaletteMock.mockResolvedValueOnce(created);
const element = await createDialog();
const setup = await enterPaletteCreationWithSetup(element, 'save-source.png');

const name = getElement<HTMLInputElement>(element, '#guided-palette-name');
name.value = 'Saved palette';
name.dispatchEvent(new Event('input'));
getElement<HTMLButtonElement>(
element,
'button[form="palette-creation-form"]',
).click();
await settle(element);
await vi.runAllTimersAsync();
await settle(element);

const restored = getGuidedSetup(element);
expect(restored.filename.textContent).toContain('save-source.png');
expect(restored.advanced.open).toBe(true);
expect(restored.longSide.value).toBe('32');
expect(restored.mapping.value).toBe('luminance');
expect(restored.simplify.checked).toBe(false);
expect(restored.select.value).toBe('custom:new-palette');
expect(restored.sourcePreview).toBeTruthy();
expect(restored.guidePreview).toBeTruthy();
expect(restored.status.textContent).toBe(setup.statusText);
expect(generateNumberedGuideMock).toHaveBeenLastCalledWith(sampledImage, {
maxColors: undefined,
palette: ['#123456'],
mapping: 'luminance',
simplifyIsolatedPixels: false,
});
expect(element.shadowRoot?.activeElement).toBe(restored.select);
});

it('selects preset and saved palettes without reading an active project palette', async () => {
const activeProjectPaletteSpy = vi.spyOn(projectContext, 'getActiveProjectContext');
const saved = customPalette('night', 'Night colors', ['#101020', '#f0f0ff']);
Expand Down
Loading