-
-
-
-
- Source status
-
-
- Allowed move targets
-
- Save
-
-
-
-
-
-
- {{
- ticketStatusLabel(sourceStatus)
- }}
-
-
-
-
-
-
-
- Save
-
-
-
-
-
+
+
+
+
+
+
diff --git a/resources/js/types/project-workflow.ts b/resources/js/types/project-workflow.ts
new file mode 100644
index 0000000..cc207c4
--- /dev/null
+++ b/resources/js/types/project-workflow.ts
@@ -0,0 +1,20 @@
+import type { BoardColumnModel } from '@/types/models/board-column-model';
+import type { TicketStatusModel } from '@/types/models/ticket-status-model';
+
+export type WorkflowEditorTarget =
+ | { kind: 'column'; action: 'create'; column?: never }
+ | { kind: 'column'; action: 'edit' | 'delete'; column: BoardColumnModel }
+ | {
+ kind: 'status';
+ action: 'create';
+ column: BoardColumnModel | null;
+ status?: never;
+ replacement?: never;
+ }
+ | {
+ kind: 'status';
+ action: 'edit' | 'delete';
+ column: BoardColumnModel | null;
+ status: TicketStatusModel;
+ replacement?: TicketStatusModel;
+ };
diff --git a/tests/Feature/Projects/ProjectBoardSettingsControllerTest.php b/tests/Feature/Projects/ProjectBoardSettingsControllerTest.php
index 994c368..37ca35f 100644
--- a/tests/Feature/Projects/ProjectBoardSettingsControllerTest.php
+++ b/tests/Feature/Projects/ProjectBoardSettingsControllerTest.php
@@ -372,4 +372,51 @@ class ProjectBoardSettingsControllerTest extends TestCase
'id' => $backlogStatus->id,
]);
}
+
+ public function test_deleting_a_used_column_preserves_statuses_tickets_and_backlog_order(): void
+ {
+ $user = User::factory()->create();
+ $project = Project::factory()->create();
+ $this->syncProjectPermissions($project, $user, ['edit']);
+ $backlog = TicketStatus::factory()->for($project)->create(['position' => 1]);
+ $column = BoardColumn::factory()->for($project)->create(['position' => 1]);
+ $statuses = TicketStatus::factory()->for($project)->for($column)->count(2)
+ ->sequence(['position' => 1], ['position' => 2])->create();
+ $ticket = Ticket::factory()->for($project)->create(['ticket_status_id' => $statuses[1]->id]);
+ $statuses[0]->allowedTargetStatuses()->sync([$statuses[1]->id]);
+
+ $this->actingAs($user)
+ ->delete(route('projects.settings.board.columns.destroy', [$project, $column]))
+ ->assertRedirect(route('projects.settings.board', $project))
+ ->assertSessionHasNoErrors();
+
+ $this->assertModelMissing($column);
+ $this->assertSame($statuses[1]->id, $ticket->fresh()->ticket_status_id);
+ $this->assertSame([$backlog->id, ...$statuses->modelKeys()], $project->ticketStatuses()->pluck('id')->all());
+ $this->assertSame([1, 2, 3], $project->ticketStatuses()->pluck('position')->all());
+ $this->assertTrue($project->ticketStatuses()->get()->every(fn (TicketStatus $status): bool => $status->board_column_id === null));
+ $this->assertSame([$statuses[1]->id], $statuses[0]->allowedTargetStatuses()->pluck('ticket_statuses.id')->all());
+ }
+
+ public function test_last_column_status_is_protected_and_invalid_position_preserves_order(): void
+ {
+ $user = User::factory()->create();
+ $project = Project::factory()->create();
+ $this->syncProjectPermissions($project, $user, ['edit']);
+ $column = BoardColumn::factory()->for($project)->create(['position' => 1]);
+ $status = TicketStatus::factory()->for($project)->for($column)->create();
+ $ticket = Ticket::factory()->for($project)->create(['ticket_status_id' => $status->id]);
+
+ $this->actingAs($user)->from(route('projects.settings.board', $project))
+ ->delete(route('projects.settings.board.statuses.destroy', [$project, $status]))
+ ->assertSessionHasErrors('ticket_status_id');
+ $this->assertModelExists($status);
+ $this->assertSame($status->id, $ticket->fresh()->ticket_status_id);
+
+ $this->actingAs($user)->from(route('projects.settings.board', $project))
+ ->patch(route('projects.settings.board.columns.update', [$project, $column]), ['name' => 'Invalid move', 'position' => 0])
+ ->assertSessionHasErrors('position');
+ $this->assertSame(1, $column->fresh()->position);
+ $this->assertSame($column->name, $column->fresh()->name);
+ }
}
diff --git a/tests/Feature/Projects/ProjectBoardSettingsUiTest.php b/tests/Feature/Projects/ProjectBoardSettingsUiTest.php
new file mode 100644
index 0000000..e007c72
--- /dev/null
+++ b/tests/Feature/Projects/ProjectBoardSettingsUiTest.php
@@ -0,0 +1,18 @@
+setTimeout(60);
+ $process->run();
+ $this->assertTrue($process->isSuccessful(), $process->getOutput().$process->getErrorOutput());
+ $this->assertStringContainsString('Project board settings rendering checks passed.', $process->getOutput());
+ }
+}
diff --git a/tests/Fixtures/project-board-settings-browser.mjs b/tests/Fixtures/project-board-settings-browser.mjs
new file mode 100644
index 0000000..f0a4511
--- /dev/null
+++ b/tests/Fixtures/project-board-settings-browser.mjs
@@ -0,0 +1,280 @@
+import assert from 'node:assert/strict';
+import process from 'node:process';
+const { chromium } = await import(
+ process.env.PLAYWRIGHT_MODULE ?? 'playwright'
+);
+const browser = await chromium.launch({ channel: 'chrome', headless: true });
+const base = 'http://127.0.0.1:4188';
+const context = await browser.newContext({
+ viewport: { width: 1440, height: 1000 },
+});
+const page = await context.newPage();
+page.setDefaultTimeout(10000);
+const errors = [];
+page.on('pageerror', (error) => errors.push(error.message));
+const requests = [];
+page.on('request', (request) => {
+ if (['POST', 'PATCH', 'DELETE'].includes(request.method())) {
+ requests.push(request);
+ }
+});
+const button = (name) => page.getByRole('button', { name, exact: true });
+const dialog = () => page.getByRole('dialog');
+const result = (value) =>
+ context.addCookies([{ name: 'workflow_test_result', value, url: base }]);
+const visit = async (query = '?reset') => {
+ await result('success');
+ await page.goto(base + '/projects/1/settings/board' + query);
+ await page
+ .getByRole('heading', { name: 'Board workflow', exact: true })
+ .waitFor();
+};
+const noOverflow = async () =>
+ assert.equal(
+ await page.evaluate(
+ () => document.documentElement.scrollWidth <= window.innerWidth,
+ ),
+ true,
+ );
+const submit = async (label, method, pathname, expected) => {
+ const sent = page.waitForRequest((request) => request.method() === method);
+ await dialog().getByRole('button', { name: label, exact: true }).click();
+ const request = await sent;
+ assert.equal(new URL(request.url()).pathname, pathname);
+ assert.deepEqual(request.postDataJSON(), expected);
+};
+const closed = () => dialog().waitFor({ state: 'hidden' });
+const formState = (state) =>
+ dialog()
+ .locator('[data-form-state="' + state + '"]')
+ .waitFor({ state: 'visible' });
+try {
+ for (const width of [1440, 768, 390, 320]) {
+ await page.setViewportSize({ width, height: 1000 });
+ for (const theme of ['light', 'dark']) {
+ await page.emulateMedia({ colorScheme: theme });
+ await visit();
+ await noOverflow();
+ assert.equal(
+ await button('Delete status Backlog').isDisabled(),
+ true,
+ );
+ assert.equal(
+ await button('Delete status Ready').isDisabled(),
+ true,
+ );
+ assert.equal(
+ await button('Delete status Review').isDisabled(),
+ false,
+ );
+ const trigger = button('Edit column In progress');
+ await trigger.focus();
+ await page.keyboard.press('Enter');
+ await dialog().waitFor();
+ assert.equal(
+ await page
+ .getByLabel('Column name', { exact: true })
+ .inputValue(),
+ 'In progress',
+ );
+ await page.keyboard.press('Tab');
+ assert.equal(
+ await dialog().evaluate((element) =>
+ element.contains(document.activeElement),
+ ),
+ true,
+ );
+ await noOverflow();
+ await page.keyboard.press('Escape');
+ await closed();
+ assert.equal(
+ await trigger.evaluate(
+ (element) => document.activeElement === element,
+ ),
+ true,
+ );
+ if ([390, 1440].includes(width)) {
+ await page.screenshot({
+ path: '/private/tmp/KAN-40-' + width + '-' + theme + '.png',
+ fullPage: true,
+ });
+ }
+ assert.equal(requests.length, 0);
+ }
+ }
+ await visit('?reset&empty');
+ await page.getByText('No board columns yet', { exact: true }).waitFor();
+ await visit();
+ await button('Add column').click();
+ await page
+ .getByLabel('Column name', { exact: true })
+ .fill('Quality assurance');
+ await result('validation');
+ await submit('Add column', 'POST', '/projects/1/settings/board/columns', {
+ name: 'Quality assurance',
+ });
+ await formState('validation');
+ assert.equal(
+ await page.getByLabel('Column name', { exact: true }).inputValue(),
+ 'Quality assurance',
+ );
+ assert.equal(
+ await page
+ .getByLabel('Column name', { exact: true })
+ .getAttribute('aria-invalid'),
+ 'true',
+ );
+ await result('server');
+ await dialog()
+ .getByRole('button', { name: 'Add column', exact: true })
+ .click();
+ await formState('server');
+ await result('success');
+ await dialog()
+ .getByRole('button', { name: 'Try again', exact: true })
+ .click();
+ await closed();
+ await button('Edit column Quality assurance').waitFor();
+ const newColumnId = await page
+ .locator('[data-column-id]')
+ .last()
+ .getAttribute('data-column-id');
+ await button('Edit column Quality assurance').click();
+ await page.getByLabel('Column name', { exact: true }).fill('QA');
+ await page.getByLabel('Position', { exact: true }).fill('1');
+ await result('position');
+ await dialog().getByRole('button', { name: 'Save', exact: true }).click();
+ await formState('validation');
+ assert.equal(
+ await page
+ .getByLabel('Position', { exact: true })
+ .getAttribute('aria-invalid'),
+ 'true',
+ );
+ await result('success');
+ await submit(
+ 'Save',
+ 'PATCH',
+ '/projects/1/settings/board/columns/' + newColumnId,
+ { name: 'QA', position: 1 },
+ );
+ await closed();
+ assert.equal(
+ await page
+ .locator('[data-column-id]')
+ .first()
+ .getAttribute('data-column-id'),
+ newColumnId,
+ );
+ await button('Edit column QA').click();
+ assert.equal(
+ await page.getByLabel('Position', { exact: true }).inputValue(),
+ '1',
+ );
+ assert.equal(
+ await page.getByLabel('Column name', { exact: true }).inputValue(),
+ 'QA',
+ );
+ await page.getByLabel('Column name', { exact: true }).fill('Unsaved');
+ page.once('dialog', (prompt) => prompt.dismiss());
+ await dialog().getByRole('button', { name: 'Cancel', exact: true }).click();
+ assert.equal(await dialog().isVisible(), true);
+ page.once('dialog', (prompt) => prompt.accept());
+ await dialog().getByRole('button', { name: 'Cancel', exact: true }).click();
+ await closed();
+ await button('Add status to Backlog').click();
+ assert.equal(
+ await page.getByLabel('Status name', { exact: true }).inputValue(),
+ '',
+ );
+ await page.getByLabel('Status name', { exact: true }).fill('Triage');
+ await submit('Add status', 'POST', '/projects/1/settings/board/statuses', {
+ name: 'Triage',
+ board_column_id: null,
+ });
+ await closed();
+ await button('Rename status Triage').waitFor();
+ await button('Add status to Todo').click();
+ await page.getByLabel('Status name', { exact: true }).fill('Refinement');
+ await submit('Add status', 'POST', '/projects/1/settings/board/statuses', {
+ name: 'Refinement',
+ board_column_id: 10,
+ });
+ await closed();
+ await button('Rename status Review').click();
+ await page.getByLabel('Status name', { exact: true }).fill('Peer review');
+ await page.route('**/statuses/4', (route) => route.abort('failed'));
+ await dialog().getByRole('button', { name: 'Save', exact: true }).click();
+ await formState('network');
+ await page.unroute('**/statuses/4');
+ await submit(
+ 'Try again',
+ 'PATCH',
+ '/projects/1/settings/board/statuses/4',
+ { name: 'Peer review' },
+ );
+ await closed();
+ const before = requests.length;
+ await button('Delete status Peer review').click();
+ await dialog()
+ .getByText(/assigned to “Building”/)
+ .waitFor();
+ await dialog().getByRole('button', { name: 'Cancel', exact: true }).click();
+ await closed();
+ assert.equal(requests.length, before);
+ await button('Delete status Peer review').click();
+ await result('validation');
+ await submit(
+ 'Delete',
+ 'DELETE',
+ '/projects/1/settings/board/statuses/4',
+ {},
+ );
+ await formState('validation');
+ await dialog()
+ .getByText('This group needs at least one status.', { exact: true })
+ .waitFor();
+ await result('success');
+ await dialog().getByRole('button', { name: 'Delete', exact: true }).click();
+ await closed();
+ assert.equal(await button('Delete status Building').isDisabled(), true);
+ await button('Delete column In progress').click();
+ await dialog()
+ .getByText(/statuses move to the backlog/)
+ .waitFor();
+ await submit(
+ 'Delete',
+ 'DELETE',
+ '/projects/1/settings/board/columns/20',
+ {},
+ );
+ await closed();
+ await page
+ .getByRole('list', { name: 'Statuses in Backlog', exact: true })
+ .getByText('Building', { exact: true })
+ .waitFor();
+ assert.equal(await button('Edit column In progress').count(), 0);
+ await page.locator('summary').filter({ hasText: 'Status moves' }).click();
+ const transition = page.locator('details form').first();
+ const checkbox = transition.getByRole('checkbox').first();
+ await checkbox.check();
+ const move = page.waitForRequest((request) => request.method() === 'POST');
+ await transition.getByRole('button', { name: 'Save', exact: true }).click();
+ assert.equal(
+ new URL((await move).url()).pathname,
+ '/projects/1/settings/board/statuses/1/transitions',
+ );
+ assert.equal(
+ new URL((await move).url()).searchParams.get('_method'),
+ 'PATCH',
+ );
+ assert.deepEqual((await move).postDataJSON(), {
+ allowed_ticket_status_ids: [await checkbox.getAttribute('value')],
+ });
+ assert.deepEqual(errors, []);
+ console.log(
+ 'Workflow browser checks passed: CRUD payloads, server refresh, order, deletion protection, validation, retries, dirty forms, keyboard, responsive widths and both themes.',
+ );
+} finally {
+ await browser.close();
+}
diff --git a/tests/Fixtures/project-board-settings-preview.mjs b/tests/Fixtures/project-board-settings-preview.mjs
new file mode 100644
index 0000000..08c7983
--- /dev/null
+++ b/tests/Fixtures/project-board-settings-preview.mjs
@@ -0,0 +1,19 @@
+import { createApp, h } from 'vue';
+import { createInertiaApp } from '@inertiajs/vue3';
+import '../../resources/css/app.css';
+import Board from '../../resources/js/pages/projects/settings/Board.vue';
+import { createAppearance } from '../../resources/js/composables/useAppearance';
+import { createLocalization } from '../../resources/js/lib/localization';
+
+createInertiaApp({
+ resolve: (name) =>
+ name === 'Board'
+ ? Board
+ : { render: () => h('h1', 'Destination reached') },
+ setup: ({ el, App, props, plugin }) =>
+ createApp({ render: () => h(App, props) })
+ .use(plugin)
+ .use(createLocalization())
+ .use(createAppearance())
+ .mount(el),
+});
diff --git a/tests/Fixtures/project-board-settings.mjs b/tests/Fixtures/project-board-settings.mjs
new file mode 100644
index 0000000..73b06d3
--- /dev/null
+++ b/tests/Fixtures/project-board-settings.mjs
@@ -0,0 +1,330 @@
+import assert from 'node:assert/strict';
+import process from 'node:process';
+import { fileURLToPath } from 'node:url';
+import vue from '@vitejs/plugin-vue';
+import tailwind from '@tailwindcss/vite';
+import { createServer } from 'vite';
+import { createSSRApp, h } from 'vue';
+import { renderToString } from 'vue/server-renderer';
+import { createInertiaApp } from '@inertiajs/vue3';
+
+const serve = process.argv.includes('--serve');
+const server = await createServer({
+ configFile: false,
+ cacheDir: 'node_modules/.vite/project-board-settings-fixture',
+ plugins: [vue(), ...(serve ? [tailwind()] : [])],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('../../resources/js', import.meta.url)),
+ },
+ },
+ server: serve
+ ? { host: '127.0.0.1', port: 4188, strictPort: true, hmr: false }
+ : { middlewareMode: true, hmr: false, watch: null },
+ appType: 'custom',
+});
+const makeStatus = (id, name, column, position = 1) => ({
+ id,
+ name,
+ board_column_id: column,
+ position,
+ allowed_target_statuses: [],
+});
+function initial() {
+ const backlog = makeStatus(1, 'Backlog', null);
+ const ready = makeStatus(2, 'Ready', 10);
+ const active = makeStatus(3, 'Building', 20);
+ const review = makeStatus(4, 'Review', 20, 2);
+ const columns = [
+ { id: 10, name: 'Todo', position: 1, ticket_statuses: [ready] },
+ {
+ id: 20,
+ name: 'In progress',
+ position: 2,
+ ticket_statuses: [active, review],
+ },
+ ];
+ return {
+ project: { id: 1, key: 'CORE', name: 'Core platform' },
+ status: null,
+ templateState: null,
+ backlogTicketStatuses: [backlog],
+ boardColumns: columns,
+ ticketStatuses: [
+ backlog,
+ ...columns.flatMap((column) =>
+ column.ticket_statuses.map((status) => ({
+ ...status,
+ board_column: { id: column.id, name: column.name },
+ })),
+ ),
+ ],
+ };
+}
+function makePage(
+ extra = {},
+ url = '/projects/1/settings/board',
+ component = 'Board',
+) {
+ return {
+ component,
+ url,
+ version: null,
+ clearHistory: false,
+ encryptHistory: false,
+ props: {
+ name: 'KanbanGit',
+ errors: {},
+ sidebarOpen: true,
+ appearance: 'system',
+ auth: {
+ user: {
+ id: 1,
+ name: 'Alex Morgan',
+ email: 'alex@example.test',
+ avatar: null,
+ avatar_color: '#10B981',
+ avatar_is_generated: true,
+ },
+ permissions: {
+ globalAdmin: false,
+ projectCreate: false,
+ canManageTeams: false,
+ },
+ },
+ navigation: {
+ project: {
+ id: 1,
+ name: 'Core platform',
+ key: 'CORE',
+ canManage: true,
+ },
+ },
+ ...initial(),
+ ...extra,
+ },
+ };
+}
+if (serve) {
+ let data = initial();
+ let nextId = 100;
+ server.middlewares.use(async (request, response, next) => {
+ if (!request.url.startsWith('/projects')) {
+ return next();
+ }
+ const url = new URL(request.url, 'http://localhost');
+ const mutation = request.method !== 'GET';
+ if (!mutation && url.searchParams.has('reset')) {
+ data = initial();
+ }
+ let errors = {};
+ if (mutation) {
+ const result =
+ request.headers.cookie?.match(
+ /workflow_test_result=(\w+)/,
+ )?.[1] ?? 'success';
+ let body = '';
+ for await (const chunk of request) {
+ body += chunk;
+ }
+ const submitted = JSON.parse(body || '{}');
+ if (['server', 'forbidden'].includes(result)) {
+ response.statusCode = result === 'server' ? 503 : 403;
+ response.end('Simulated request failure');
+ return;
+ }
+ if (result === 'validation') {
+ errors =
+ request.method === 'DELETE'
+ ? {
+ ticket_status_id:
+ 'This group needs at least one status.',
+ }
+ : { name: 'Choose a different name.' };
+ } else if (result === 'position') {
+ errors = { position: 'The position must be at least 1.' };
+ } else {
+ const segments = url.pathname.split('/');
+ const id = Number(segments.at(-1));
+ const columns = data.boardColumns;
+ if (url.pathname.includes('/columns')) {
+ if (request.method === 'POST') {
+ const column = {
+ id: nextId++,
+ name: submitted.name,
+ position: columns.length + 1,
+ ticket_statuses: [],
+ };
+ column.ticket_statuses.push(
+ makeStatus(nextId++, submitted.name, column.id),
+ );
+ columns.push(column);
+ } else if (request.method === 'PATCH') {
+ const column = columns.find((item) => item.id === id);
+ column.name = submitted.name;
+ columns.splice(columns.indexOf(column), 1);
+ columns.splice(
+ Math.max(
+ 0,
+ Math.min(
+ Number(submitted.position) - 1,
+ columns.length,
+ ),
+ ),
+ 0,
+ column,
+ );
+ } else {
+ const column = columns.find((item) => item.id === id);
+ for (const status of column.ticket_statuses) {
+ status.board_column_id = null;
+ status.position =
+ data.backlogTicketStatuses.length + 1;
+ data.backlogTicketStatuses.push(status);
+ }
+ columns.splice(columns.indexOf(column), 1);
+ }
+ columns.forEach((column, index) => {
+ column.position = index + 1;
+ });
+ } else if (url.pathname.endsWith('/transitions')) {
+ // The browser assertion checks the preserved transition endpoint and payload.
+ } else if (request.method === 'POST') {
+ const group =
+ submitted.board_column_id === null
+ ? data.backlogTicketStatuses
+ : columns.find(
+ (item) =>
+ item.id === submitted.board_column_id,
+ ).ticket_statuses;
+ group.push(
+ makeStatus(
+ nextId++,
+ submitted.name,
+ submitted.board_column_id,
+ group.length + 1,
+ ),
+ );
+ } else {
+ const group = [
+ data.backlogTicketStatuses,
+ ...columns.map((column) => column.ticket_statuses),
+ ].find((items) => items.some((item) => item.id === id));
+ const status = group.find((item) => item.id === id);
+ if (request.method === 'PATCH') {
+ status.name = submitted.name;
+ } else {
+ group.splice(group.indexOf(status), 1);
+ }
+ }
+ data.ticketStatuses = [
+ ...data.backlogTicketStatuses,
+ ...columns.flatMap((column) =>
+ column.ticket_statuses.map((status) => ({
+ ...status,
+ board_column: { id: column.id, name: column.name },
+ })),
+ ),
+ ];
+ data.status = 'Updated';
+ }
+ }
+ if (url.searchParams.has('empty')) {
+ data.boardColumns = [];
+ data.ticketStatuses = data.backlogTicketStatuses;
+ }
+ const page = makePage({ ...data, errors });
+ response.setHeader('Vary', 'X-Inertia');
+ if (request.headers['x-inertia']) {
+ response.setHeader('X-Inertia', 'true');
+ response.setHeader('Content-Type', 'application/json');
+ response.end(JSON.stringify(page));
+ return;
+ }
+ const escaped = JSON.stringify(page)
+ .replaceAll('&', '&')
+ .replaceAll('"', '"')
+ .replaceAll('<', '<');
+ response.setHeader('Content-Type', 'text/html');
+ response.end(
+ await server.transformIndexHtml(
+ request.url,
+ '
',
+ ),
+ );
+ });
+ await server.listen();
+ console.log('Project board settings fixture ready.');
+} else {
+ try {
+ const { createLocalization } = await server.ssrLoadModule(
+ '/resources/js/lib/localization.ts',
+ );
+ const { createAppearance } = await server.ssrLoadModule(
+ '/resources/js/composables/useAppearance.ts',
+ );
+ const { en } = await server.ssrLoadModule(
+ '/resources/js/locales/en.ts',
+ );
+ const component = (
+ await server.ssrLoadModule(
+ '/resources/js/pages/projects/settings/Board.vue',
+ )
+ ).default;
+ const render = async (extra = {}, locale = createLocalization()) =>
+ (
+ await createInertiaApp({
+ page: makePage(extra),
+ resolve: () => component,
+ render: renderToString,
+ setup: ({ App, props, plugin }) =>
+ createSSRApp({ render: () => h(App, props) })
+ .use(plugin)
+ .use(locale)
+ .use(createAppearance()),
+ })
+ ).body;
+ const html = await render();
+ assert.match(html, /Board workflow/);
+ assert.match(html, /Off the board/);
+ assert.match(html, /aria-current="page"/);
+ assert.ok(
+ html.indexOf('data-column-id="10"') <
+ html.indexOf('data-column-id="20"'),
+ );
+ const buttons = html.match(/
]*>[\s\S]*?<\/button>/g);
+ for (const name of ['Backlog', 'Ready']) {
+ assert.match(
+ buttons.find((button) =>
+ button.includes('aria-label="Delete status ' + name + '"'),
+ ),
+ /aria-disabled="true"/,
+ );
+ }
+ assert.doesNotMatch(
+ buttons.find((button) =>
+ button.includes('aria-label="Delete status Review"'),
+ ),
+ /aria-disabled="true"/,
+ );
+ assert.match(
+ await render({ boardColumns: [] }),
+ /No board columns yet/,
+ );
+ assert.match(
+ await render(
+ {},
+ createLocalization(
+ { locale: 'de' },
+ { de: { ...en, 'workflow.title': 'Board-Ablauf' } },
+ ),
+ ),
+ /Board-Ablauf/,
+ );
+ console.log('Project board settings rendering checks passed.');
+ } finally {
+ await server.close();
+ }
+}