Files
nexu-io__open-design/apps/web/tests/components/DesignSystemsSection.test.tsx
Amy 028bde50ad fix(release): harden prerelease E2E validation (#6755)
* fix(release): harden prerelease E2E validation

* fix(daemon): refresh active run tool tokens

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* chore(release): keep Feishu notification unchanged

* fix(daemon): retain media tasks for active runs

Associate token-authored media tasks with their owning run and defer terminal cleanup while that run still has an active tool token. Correct the prerelease status document to reflect the final Feishu notification topology.

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* test(e2e): defer system theme coverage

* test(e2e): remove retired design system settings flows

* fix(e2e): close prerelease coverage gaps

* fix(release): remove unrelated UI changes

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* fix(e2e): restore remaining prerelease coverage

* docs(e2e): record remaining scoped gaps

* test(e2e): isolate workflow command fixtures

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* test(e2e): preserve Node in workflow fixture PATH

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* fix(ci): skip Playwright apt deps on Blacksmith runners

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* Revert "fix(ci): skip Playwright apt deps on Blacksmith runners"

This reverts commit 201f3f3ee3.

* fix(plan): reliably open generated artifacts

* fix(plan): reject unconfirmed artifact paths
2026-08-12 11:29:11 +00:00

202 lines
7.3 KiB
TypeScript

// @vitest-environment jsdom
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import postcss, { type Rule } from 'postcss';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { DesignSystemSummary } from '@open-design/contracts';
import { DesignSystemsSection } from '../../src/components/DesignSystemsSection';
import { fetchDesignSystems, updateDesignSystemDraft } from '../../src/providers/registry';
import type { AppConfig } from '../../src/types';
const memoryCss = readFileSync(resolve(process.cwd(), 'src/styles/viewer/memory.css'), 'utf8');
const memoryCssRoot = postcss.parse(memoryCss, { from: 'src/styles/viewer/memory.css' });
function topLevelRuleWithProperty(selector: string, property: string): Rule {
const match = memoryCssRoot.nodes.find((node): node is Rule => {
if (node.type !== 'rule') return false;
if (!node.selectors.includes(selector)) return false;
return node.nodes.some((child) => child.type === 'decl' && child.prop === property);
});
if (!match) throw new Error(`Missing CSS block for ${selector}`);
return match;
}
function ruleValue(rule: Rule, property: string): string {
const decl = rule.nodes.find(
(node) => node.type === 'decl' && node.prop === property,
);
if (!decl || decl.type !== 'decl') throw new Error(`Missing CSS property ${property}`);
return decl.value.trim();
}
const editable: DesignSystemSummary = {
id: 'user:acme',
title: 'Acme Design System',
category: 'Custom',
summary: 'Internal product system.',
surface: 'web',
source: 'user',
status: 'draft',
isEditable: true,
updatedAt: '2026-05-13T03:19:00.000Z',
};
const builtIn: DesignSystemSummary = {
id: 'linear',
title: 'Linear',
category: 'Productivity & SaaS',
summary: 'Quiet issue-tracker system.',
surface: 'web',
source: 'built-in',
status: 'published',
isEditable: false,
};
vi.mock('../../src/providers/registry', async () => {
const actual = await vi.importActual<typeof import('../../src/providers/registry')>(
'../../src/providers/registry',
);
return {
...actual,
fetchDesignSystems: vi.fn(async () => [editable, builtIn]),
updateDesignSystemDraft: vi.fn(async () => ({ ...editable, title: 'Acme v2', body: '' })),
};
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
const cfg = { disabledDesignSystems: [] } as unknown as AppConfig;
describe('DesignSystemsSection rename (issue #2811)', () => {
it('renames an editable design system through the internal component', async () => {
render(<DesignSystemsSection cfg={cfg} setCfg={() => {}} />);
const renameButton = await screen.findByRole('button', {
name: /Rename Acme Design System/i,
});
fireEvent.click(renameButton);
const input = screen.getByDisplayValue('Acme Design System');
fireEvent.change(input, { target: { value: 'Acme v2' } });
fireEvent.click(screen.getByRole('button', { name: /^Save$/ }));
await waitFor(() => {
expect(vi.mocked(updateDesignSystemDraft)).toHaveBeenCalledWith('user:acme', {
title: 'Acme v2',
}, null);
});
});
it('notifies the parent after renaming a design system without changing its id', async () => {
const onDesignSystemsChanged = vi.fn();
render(
<DesignSystemsSection
cfg={cfg}
setCfg={() => {}}
onDesignSystemsChanged={onDesignSystemsChanged}
/>,
);
const renameButton = await screen.findByRole('button', {
name: /Rename Acme Design System/i,
});
fireEvent.click(renameButton);
const input = screen.getByDisplayValue('Acme Design System');
fireEvent.change(input, { target: { value: 'Acme v2' } });
fireEvent.click(screen.getByRole('button', { name: /^Save$/ }));
await waitFor(() => {
expect(onDesignSystemsChanged).toHaveBeenCalledWith('user:acme');
});
});
it('keeps the rename modal open with the typed title when the update fails', async () => {
vi.mocked(updateDesignSystemDraft).mockResolvedValueOnce(null);
render(<DesignSystemsSection cfg={cfg} setCfg={() => {}} />);
const renameButton = await screen.findByRole('button', {
name: /Rename Acme Design System/i,
});
fireEvent.click(renameButton);
const input = screen.getByDisplayValue('Acme Design System');
fireEvent.change(input, { target: { value: 'Acme v2' } });
fireEvent.click(screen.getByRole('button', { name: /^Save$/ }));
// A failed update must not close the modal; the typed title stays for retry.
await screen.findByText(/Rename failed/i);
expect(screen.getByDisplayValue('Acme v2')).toBeTruthy();
});
it('ignores a stale rename completion when a newer rename session is open', async () => {
const editableB: DesignSystemSummary = { ...editable, id: 'user:beta', title: 'Beta System' };
vi.mocked(fetchDesignSystems).mockResolvedValueOnce([editable, editableB, builtIn]);
let resolveFirst!: (value: null) => void;
vi.mocked(updateDesignSystemDraft).mockImplementationOnce(
() =>
new Promise<null>((resolve) => {
resolveFirst = resolve;
}),
);
render(<DesignSystemsSection cfg={cfg} setCfg={() => {}} />);
// Session 1: rename Acme and submit; the PATCH stays pending.
fireEvent.click(await screen.findByRole('button', { name: /Rename Acme Design System/i }));
fireEvent.change(screen.getByDisplayValue('Acme Design System'), { target: { value: 'Acme v2' } });
fireEvent.click(screen.getByRole('button', { name: /^Save$/ }));
// Cancel Acme and open a rename for Beta before the first PATCH resolves.
fireEvent.click(screen.getByRole('button', { name: /^Cancel$/ }));
fireEvent.click(await screen.findByRole('button', { name: /Rename Beta System/i }));
expect(screen.getByDisplayValue('Beta System')).toBeTruthy();
// The stale Acme request now fails; it must not touch Beta's modal.
resolveFirst(null);
await Promise.resolve();
await Promise.resolve();
expect(screen.getByDisplayValue('Beta System')).toBeTruthy();
expect(screen.queryByText(/Rename failed/i)).toBeNull();
});
it('offers no Rename for built-in (read-only) design systems', async () => {
render(<DesignSystemsSection cfg={cfg} setCfg={() => {}} />);
await screen.findByText('Linear');
expect(screen.queryByRole('button', { name: /Rename Linear/i })).toBeNull();
});
it('keeps segmented control columns in sync with rendered buttons', async () => {
render(<DesignSystemsSection cfg={cfg} setCfg={() => {}} />);
await screen.findByText('Linear');
for (const selector of [
'.library-install-form .library-import-source-control',
'.library-install-form .library-import-mode-control',
]) {
const control = document.querySelector(selector);
expect(control).toBeTruthy();
const buttonCount = control!.querySelectorAll('button').length;
expect(buttonCount).toBeGreaterThan(0);
const gridTemplateColumns = ruleValue(
topLevelRuleWithProperty(selector, 'grid-template-columns'),
'grid-template-columns',
);
const repeatMatch = /^repeat\((\d+),\s*auto\)$/.exec(gridTemplateColumns);
expect(repeatMatch).toBeTruthy();
expect(Number(repeatMatch![1])).toBe(buttonCount);
}
});
});