From e5e8acbc40acdba59b84163452e0c316ae2c8925 Mon Sep 17 00:00:00 2001
From: Jeff Spahr Rendered report content Deterministic report content. Something went wrong.Isolated report
TensorBoard fixture
',
+ });
+ }
+ if (path === '/apps/tensorboard') {
+ assert.equal(query.namespace, 'team-a');
+ assert.equal(
+ query.logdir,
+ options.tensorboardMutations ? tensorboardLogdir : 's3://artifact-bucket/tensorboard',
+ );
+ if (request.method() === 'GET') {
+ if (options.holdTensorboardCheck) await tensorboardCheck;
+ return json({ proxyPath: '', image: '' });
+ }
+ assert.ok(options.tensorboardMutations);
+ if (request.method() === 'POST') {
+ assert.equal(query.image, tensorboardImage);
+ assert.deepEqual(JSON.parse(query.podtemplatespec), tensorboardPodTemplate);
+ assert.equal(request.headers()['content-type'], 'application/json');
+ return route.fulfill({
+ status: fixture.failTensorboardStart ? 403 : 200,
+ body: fixture.failTensorboardStart
+ ? 'Start forbidden by fixture'
+ : 'apps/tensorboard/proxy/fixture-token/',
+ });
+ }
+ assert.equal(request.method(), 'DELETE');
+ return route.fulfill({
+ status: fixture.failTensorboardStop ? 403 : 200,
+ body: fixture.failTensorboardStop ? 'Stop forbidden by fixture' : '',
+ });
+ }
+ throw new Error(`Unexpected request: ${request.method()} ${path}`);
+ } catch (error) {
+ fixture.errors.push(error.message);
+ await json({ message: error.message }, 500);
+ }
+ });
+ try {
+ await exercise(page, fixture);
+ assert.deepEqual(fixture.errors, [], 'application and HTTP contracts must remain valid');
+ } catch (error) {
+ if (fixture.errors.length) console.error('Fixture errors:', fixture.errors);
+ throw error;
+ } finally {
+ fixture.releaseTensorboardCheck();
+ await context.close();
+ }
+}
+
+async function ready(page, count) {
+ await page.getByRole('table', { name: 'Artifacts', exact: true }).waitFor();
+ await page.waitForFunction((expected) => {
+ const table = document.querySelector('table[aria-label="Artifacts"]');
+ return (
+ table?.getAttribute('aria-busy') === 'false' &&
+ table.querySelectorAll('[data-row-id]').length === expected
+ );
+ }, count);
+}
+async function screenshot(page, name) {
+ if (!process.env.KFP_ARTIFACTS_SCREENSHOT_DIR) return;
+ await mkdir(process.env.KFP_ARTIFACTS_SCREENSHOT_DIR, { recursive: true });
+ await page.screenshot({
+ path: join(process.env.KFP_ARTIFACTS_SCREENSHOT_DIR, `${name}.png`),
+ animations: 'disabled',
+ });
+}
+
+test('Artifacts combines scoped type/name filters with token paging, sorting, and refresh recovery', async () => {
+ await withFixture({ namespace: 'team-a' }, async (page, fixture) => {
+ fixture.failList = true;
+ await page.goto(`${origin}/#/artifacts`);
+ await page.getByRole('alert').waitFor();
+ fixture.failList = false;
+ await page.getByRole('button', { name: 'Refresh', exact: true }).click();
+ await ready(page, 16);
+ assert.equal(await page.getByRole('combobox', { name: 'Rows per page' }).inputValue(), '20');
+ // Select a smaller page explicitly so this fixture exercises token pagination.
+ await page.getByRole('combobox', { name: 'Rows per page' }).selectOption('10');
+ await ready(page, 10);
+ await page.getByRole('alert').waitFor({ state: 'hidden' });
+ assert.equal(
+ fixture.requests.filter((item) => item.path.startsWith('/apis/v2/artifacts/')).length,
+ 0,
+ 'list rows must not fetch per-artifact details',
+ );
+ await screenshot(page, 'artifacts-light');
+ await page.getByRole('button', { name: 'Next page', exact: true }).click();
+ await ready(page, 6);
+ await page.getByRole('searchbox', { name: 'Search artifacts' }).fill('Artifact 1');
+ await ready(page, 7);
+ assert.ok(
+ !fixture.requests.filter((item) => item.path === '/apis/v2/artifacts').at(-1).query
+ .page_token,
+ );
+ await page.getByRole('button', { name: 'Name', exact: true }).click();
+ await page.getByRole('columnheader', { name: 'Name', exact: true }).waitFor();
+ await page.getByRole('button', { name: 'Filter type', exact: true }).click();
+ await page.getByRole('menuitemradio', { name: 'Metrics', exact: true }).click();
+ await ready(page, 3);
+ const query = fixture.requests
+ .filter((item) => item.path === '/apis/v2/artifacts')
+ .at(-1).query;
+ assert.equal(query.namespace, 'team-a');
+ assert.equal(query.sort_by, 'name');
+ assert.deepEqual(
+ decodeFilter(query.filter).find((predicate) => predicate.key === 'type').int_values.values,
+ [6, 7, 8],
+ );
+ await page.getByRole('button', { name: 'Clear Type: Metrics filter', exact: true }).click();
+ await ready(page, 7);
+ await page.getByRole('button', { name: 'Clear filter', exact: true }).click();
+ await ready(page, 10);
+ await page.getByRole('combobox', { name: 'Rows per page' }).selectOption('20');
+ await ready(page, 16);
+ await page.evaluate(() => window.selectFixtureNamespace('team-b'));
+ await ready(page, 2);
+ assert.equal(
+ fixture.requests.filter((item) => item.path === '/apis/v2/artifacts').at(-1).query
+ .namespace,
+ 'team-b',
+ );
+ });
+});
+
+test('Artifact details preserves encoded identity, bounded preview consent, provider-aware download, and read recovery', async () => {
+ await withFixture({}, async (page, fixture) => {
+ fixture.failArtifact = true;
+ await page.goto(`${origin}/#${detailPath}`);
+ await page.getByRole('alert').waitFor();
+ fixture.failArtifact = false;
+ await page.getByRole('button', { name: 'Refresh', exact: true }).click();
+ await page.getByRole('region', { name: 'Artifact details', exact: true }).waitFor();
+ await page.getByRole('alert').waitFor({ state: 'hidden' });
+ assert.equal(fixture.requests.filter((item) => item.path === '/artifacts/get').length, 0);
+ const download = page.locator('a[download]');
+ const url = new URL(await download.getAttribute('href'), origin);
+ assert.equal(url.searchParams.get('download'), 'true');
+ assert.equal(url.searchParams.get('namespace'), 'team-a');
+ assert.equal(
+ url.searchParams.get('artifactUriQuery'),
+ 'endpoint=https%3A%2F%2Fstorage.example',
+ );
+ await page.getByText('Full URI', { exact: true }).click();
+ await page.getByText(artifacts[0].uri, { exact: true }).waitFor();
+ fixture.failPreview = true;
+ await page.getByRole('button', { name: 'Preview file contents', exact: true }).click();
+ await page.getByText('Error in retrieving artifact preview.', { exact: true }).waitFor();
+ fixture.failPreview = false;
+ await page.getByRole('button', { name: 'Retry preview', exact: true }).click();
+ await page.getByText('sample,value\nrow,42', { exact: true }).waitFor();
+ assert.equal(fixture.requests.filter((item) => item.path === '/artifacts/get').length, 2);
+ await page.getByRole('switch', { name: 'Dark mode', exact: true }).setChecked(true);
+ await screenshot(page, 'artifact-details-dark');
+ });
+});
+
+test('Related artifact tasks preserve producer/root semantics, encoded task destinations, and pagination', async () => {
+ await withFixture({}, async (page, fixture) => {
+ await page.goto(`${origin}/#${detailPath}/lineage`);
+ const table = page.getByRole('table', { name: 'Related tasks', exact: true });
+ const produced = table.getByRole('link', {
+ name: 'Artifact training run · Prepare dataset',
+ exact: true,
+ });
+ await produced.waitFor();
+ assert.equal(
+ await produced.getAttribute('href'),
+ '#/runs/details/run%2Fone?task=task%2Fproducer',
+ );
+ await table
+ .getByRole('link', { name: 'Artifact training run · Train model', exact: true })
+ .waitFor();
+ assert.equal(
+ fixture.requests.filter((item) => item.path === '/apis/v2/runs/run%2Fone/tasks').length,
+ 1,
+ 'related rows share task-name reads',
+ );
+ await page.getByRole('button', { name: 'Next page', exact: true }).click();
+ await table
+ .getByRole('link', { name: 'Artifact training run · Run output (root)', exact: true })
+ .waitFor();
+ assert.equal(
+ await page.getByRole('button', { name: 'Next page', exact: true }).isEnabled(),
+ false,
+ );
+ await page.getByRole('tab', { name: 'Overview', exact: true }).click();
+ await page.waitForURL((url) => url.hash === `#${detailPath}`);
+ await page.goBack();
+ await table.waitFor();
+ });
+});
+
+test('Artifact lineage bounds neighborhoods, preserves directed identity and history, and recovers reads', async () => {
+ await withFixture({}, async (page, fixture) => {
+ await page.goto(`${origin}/#${detailPath}/explorer`);
+ const graph = page.getByRole('region', { name: 'Lineage graph' });
+ const input = graph.getByRole('button', { name: 'Source dataset', exact: true });
+ await input.waitFor();
+ await graph.getByRole('button', { name: 'Trained model', exact: true }).waitFor();
+ await page.waitForFunction(
+ () => document.querySelectorAll('svg path[data-from][data-to]').length === 4,
+ );
+ assert.deepEqual(
+ await graph
+ .locator('svg path[data-from][data-to]')
+ .evaluateAll((edges) => edges.map((edge) => [edge.dataset.from, edge.dataset.to]).sort()),
+ [
+ ['task:produced', `target:${targetId}`],
+ ['task:produced:artifact:input-edge', 'task:produced'],
+ [`target:${targetId}`, 'task:consumed'],
+ ['task:consumed', 'task:consumed:artifact:output-edge'],
+ ].sort(),
+ 'rendered arrows retain producer, consumer, and adjacent artifact identities',
+ );
+ assert.ok(
+ fixture.requests
+ .filter((item) => item.path === '/apis/v2/artifact_tasks')
+ .every((item) => item.query.page_size === '5' && !item.query.page_token),
+ );
+ assert.equal(
+ await graph.getByRole('link', { name: 'Prepare dataset', exact: true }).getAttribute('href'),
+ '#/runs/details/run%2Fone?task=task%2Fproducer',
+ );
+ await input.click();
+ await page.getByText('Neighborhood 2', { exact: true }).waitFor({ state: 'attached' });
+ await graph.getByText('No recorded relationships.', { exact: true }).waitFor();
+ await page
+ .getByRole('navigation', { name: 'Lineage history', exact: true })
+ .getByRole('button', { name: 'Back', exact: true })
+ .click();
+ await input.waitFor();
+ fixture.failRelationships = true;
+ await page.getByRole('button', { name: 'Load more relationships', exact: true }).click();
+ await page.getByText('Some relationships could not be loaded.', { exact: false }).waitFor();
+ assert.equal(await input.isVisible(), true, 'loaded neighborhood survives a next-page failure');
+ fixture.failRelationships = false;
+ await page.getByRole('button', { name: 'Retry relationships', exact: true }).click();
+ await page
+ .getByText('Some relationships could not be loaded.', { exact: false })
+ .waitFor({ state: 'hidden' });
+ await page.getByRole('switch', { name: 'Dark mode', exact: true }).setChecked(true);
+ await page.waitForFunction(() => {
+ const canvas = document.querySelector('.kfp-lineage-canvas');
+ const artifact = document.querySelector('.kfp-lineage-artifact');
+ const brand = document.querySelector('.kfp-shell-brand-title');
+ return (
+ getComputedStyle(canvas).backgroundColor === 'rgb(14, 16, 21)' &&
+ getComputedStyle(artifact).backgroundColor === 'rgb(22, 25, 33)' &&
+ getComputedStyle(brand).color === 'rgb(236, 238, 244)'
+ );
+ });
+ await screenshot(page, 'artifact-lineage-dark');
+ await page.setViewportSize({ width: 600, height: 900 });
+ assert.equal(
+ await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
+ true,
+ );
+ assert.equal(
+ await graph.evaluate((element) => element.scrollWidth > element.clientWidth),
+ true,
+ );
+ await screenshot(page, 'artifact-lineage-narrow');
+ });
+});
+
+test('Legacy artifact metadata retains its namespace-scoped TensorBoard viewer', async () => {
+ await withFixture({ holdTensorboardCheck: true }, async (page, fixture) => {
+ const isTensorboardCheck = (request) =>
+ new URL(request.url()).pathname === '/apps/tensorboard' && request.method() === 'GET';
+ const checkRequested = page.waitForRequest(isTensorboardCheck);
+ const checkCompleted = page.waitForResponse((response) =>
+ isTensorboardCheck(response.request()),
+ );
+ await page.goto(`${origin}/#/artifacts/legacy`);
+ const start = page.getByRole('button', { name: 'Start Tensorboard', exact: true });
+ await start.waitFor();
+ await checkRequested;
+ // Visibility precedes the app lookup; a held response must keep Start unavailable.
+ assert.equal(await start.isEnabled(), false);
+ assert.equal(await start.getAttribute('aria-busy'), 'true');
+ fixture.releaseTensorboardCheck();
+ assert.equal((await checkCompleted).status(), 200);
+ await page
+ .getByRole('button', { name: 'Start Tensorboard', exact: true, disabled: false })
+ .waitFor();
+ assert.equal(await start.getAttribute('aria-busy'), null);
+ assert.equal(fixture.requests.filter((item) => item.path === '/apps/tensorboard').length, 1);
+ assert.equal(
+ fixture.requests.filter((item) => item.path === '/apps/tensorboard')[0].query.namespace,
+ 'team-a',
+ );
+ });
+});
+
+test('Embedded artifacts retain their prefix and active namespace', async () => {
+ await withFixture({ namespace: 'team-a', embedded: true }, async (page, fixture) => {
+ await page.goto(`${origin}/embed`);
+ const app = page.frameLocator('iframe');
+ await app.getByRole('link', { name: 'Artifact 01', exact: true }).waitFor();
+ assert.equal(await app.getByRole('complementary').count(), 0);
+ await app.getByRole('link', { name: 'Artifact 01', exact: true }).click();
+ await app.getByRole('region', { name: 'Artifact details', exact: true }).waitFor();
+ await app.getByRole('button', { name: 'Preview file contents', exact: true }).click();
+ await app.getByText('sample,value\nrow,42', { exact: true }).waitFor();
+ const preview = fixture.requests.find((item) => item.path === '/artifacts/get');
+ assert.equal(preview.pathname, '/pipeline/artifacts/get');
+ assert.equal(preview.query.namespace, 'team-a');
+ });
+});
+
+test('Rich artifact viewers preserve table paging/sort, Markdown content, and isolated HTML', async () => {
+ await withFixture({ richViewers: true }, async (page, fixture) => {
+ await page.goto(`${origin}/#/artifacts/legacy`);
+ const table = page.getByRole('table', { name: 'Table output', exact: true });
+ await table.getByRole('cell', { name: 'metric-00', exact: true }).waitFor();
+ assert.equal(await table.locator('tbody tr.kfp-viewer-table-data').count(), 10);
+ await table.getByRole('button', { name: 'Metric', exact: true }).click();
+ assert.equal(
+ await table.locator('tbody tr').first().locator('td').first().textContent(),
+ 'metric-14',
+ );
+ assert.equal(
+ await table
+ .getByRole('columnheader', { name: 'Metric', exact: true })
+ .getAttribute('aria-sort'),
+ 'descending',
+ );
+ await page.getByRole('button', { name: 'Go to next page', exact: true }).click();
+ await page.getByText('11–15 of 15', { exact: true }).waitFor();
+ await table.getByRole('cell', { name: 'metric-04', exact: true }).waitFor();
+ await page.getByRole('combobox', { name: 'Rows per page', exact: true }).selectOption('25');
+ await page.getByText('1–15 of 15', { exact: true }).waitFor();
+ assert.equal(await table.locator('tbody tr.kfp-viewer-table-data').count(), 15);
+ assert.equal(
+ await page.getByRole('button', { name: 'Go to previous page', exact: true }).isEnabled(),
+ false,
+ );
+ const markdown = page.locator('.kfp-markdown-viewer');
+ await markdown.getByRole('heading', { name: 'Viewer report', exact: true }).waitFor();
+ assert.equal(
+ await markdown
+ .locator('strong')
+ .allTextContents()
+ .then((text) => text.join()),
+ 'Measured values',
+ );
+ await markdown.getByText('Literal markup', { exact: true }).waitFor();
+ const externalLink = markdown.getByRole('link', { name: 'External documentation' });
+ assert.equal(await externalLink.getAttribute('href'), 'https://example.com/report');
+ assert.equal(await externalLink.getAttribute('target'), '_blank');
+ assert.match(await externalLink.getAttribute('rel'), /noopener/);
+ const report = page.locator('iframe[title="HTML report"]');
+ await report.contentFrame().getByRole('heading', { name: 'Isolated report' }).waitFor();
+ assert.equal(await report.getAttribute('sandbox'), 'allow-scripts');
+ assert.equal(await report.getAttribute('src'), 'about:blank');
+ assert.equal(
+ fixture.requests.filter((item) => item.path === '/artifacts/get').length,
+ 1,
+ 'inline metadata content needs no extra source fetch',
+ );
+ const lightText = await table.evaluate((element) => getComputedStyle(element).color);
+ await page.getByRole('switch', { name: 'Dark mode', exact: true }).setChecked(true);
+ await page.waitForFunction((lightColor) => {
+ const table = document.querySelector('.kfp-theme.dark table[aria-label="Table output"]');
+ const theme = table?.closest('.kfp-theme');
+ const cell = table?.querySelector('tbody td');
+ return (
+ cell &&
+ theme &&
+ getComputedStyle(cell).color !== lightColor &&
+ getComputedStyle(cell).color === getComputedStyle(theme).color
+ );
+ }, lightText);
+ const border = await table
+ .locator('tbody tr')
+ .first()
+ .locator('td')
+ .first()
+ .evaluate((cell) => {
+ const style = getComputedStyle(cell);
+ return {
+ left: style.borderLeftWidth,
+ right: style.borderRightWidth,
+ color: style.color,
+ background: getComputedStyle(cell.closest('.kfp-theme')).backgroundColor,
+ themeText: getComputedStyle(cell.closest('.kfp-theme')).color,
+ };
+ });
+ assert.deepEqual([border.left, border.right], ['1px', '1px']);
+ assert.notEqual(border.color, lightText, 'table foreground follows the theme change');
+ assert.equal(border.color, border.themeText, 'table inherits the active theme foreground');
+ assert.notEqual(border.color, border.background, 'table text remains distinct in dark mode');
+ await table.scrollIntoViewIfNeeded();
+ await screenshot(page, 'artifact-rich-viewers-dark');
+ await markdown.scrollIntoViewIfNeeded();
+ await screenshot(page, 'artifact-markdown-dark');
+ await report.scrollIntoViewIfNeeded();
+ await screenshot(page, 'artifact-html-dark');
+ await page.setViewportSize({ width: 600, height: 900 });
+ assert.equal(
+ await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
+ true,
+ );
+ });
+});
+
+test('TensorBoard preserves PVC/image/pod-template contracts, proxy readiness, cancellation, and mutation recovery', async () => {
+ await withFixture({ namespace: 'team-a', tensorboardMutations: true }, async (page, fixture) => {
+ fixture.failTensorboardStart = true;
+ await page.goto(`${origin}/#/artifacts/legacy`);
+ const image = page.getByRole('combobox', { name: 'TF Image' });
+ await image.waitFor();
+ assert.equal(await image.inputValue(), tensorboardImage);
+ await page.getByRole('button', { name: 'Start Tensorboard', exact: true }).click();
+ await page.getByRole('alert').filter({ hasText: 'Start forbidden by fixture' }).waitFor();
+ fixture.failTensorboardStart = false;
+ const notReady = page.waitForResponse(
+ (response) =>
+ new URL(response.url()).pathname === '/apps/tensorboard/proxy/fixture-token/' &&
+ response.request().method() === 'HEAD' &&
+ response.status() === 503,
+ );
+ await page.getByRole('button', { name: 'Start Tensorboard', exact: true }).click();
+ await notReady;
+ const open = page.getByRole('link', { name: 'Open Tensorboard', exact: true });
+ await open.waitFor();
+ await page
+ .getByRole('alert')
+ .filter({ hasText: 'Start forbidden by fixture' })
+ .waitFor({ state: 'hidden' });
+ assert.equal(await open.getAttribute('href'), 'apps/tensorboard/proxy/fixture-token/');
+ assert.equal(await open.getAttribute('target'), '_blank');
+ await page
+ .getByText('Tensorboard is starting, and you may need to wait for a few minutes.', {
+ exact: true,
+ })
+ .waitFor();
+ const ready = page.waitForResponse(
+ (response) =>
+ new URL(response.url()).pathname === '/apps/tensorboard/proxy/fixture-token/' &&
+ response.request().method() === 'HEAD' &&
+ response.status() === 200,
+ );
+ fixture.tensorboardReady = true;
+ await ready;
+ await page
+ .getByText('Tensorboard is starting, and you may need to wait for a few minutes.', {
+ exact: true,
+ })
+ .waitFor({ state: 'hidden' });
+ const popupPromise = page.waitForEvent('popup');
+ await open.click();
+ const popup = await popupPromise;
+ await popup.getByRole('heading', { name: 'TensorBoard fixture' }).waitFor();
+ assert.equal(new URL(popup.url()).pathname, '/apps/tensorboard/proxy/fixture-token/');
+ await popup.close();
+ await page.getByRole('switch', { name: 'Dark mode', exact: true }).setChecked(true);
+ const stop = page.getByRole('button', { name: 'Stop Tensorboard', exact: true });
+ await stop.focus();
+ await stop.press('Enter');
+ const dialog = page.getByRole('dialog', { name: 'Stop Tensorboard?' });
+ const cancel = dialog.getByRole('button', { name: 'Cancel', exact: true });
+ await cancel.waitFor();
+ await page.waitForFunction(() => document.activeElement?.textContent === 'Cancel');
+ assert.equal(await dialog.evaluate((element) => !!element.closest('.kfp-theme.dark')), true);
+ await screenshot(page, 'artifact-tensorboard-stop-dark');
+ await cancel.press('Shift+Tab');
+ await page.waitForFunction(() => document.activeElement?.textContent === 'Stop');
+ await page.keyboard.press('Tab');
+ await page.waitForFunction(() => document.activeElement?.textContent === 'Cancel');
+ await cancel.click();
+ await dialog.waitFor({ state: 'hidden' });
+ await page.waitForFunction(() => document.activeElement?.textContent === 'Stop Tensorboard');
+ assert.equal(fixture.requests.filter((item) => item.method === 'DELETE').length, 0);
+ fixture.failTensorboardStop = true;
+ await stop.click();
+ await dialog.getByRole('button', { name: 'Stop', exact: true }).click();
+ await dialog.getByRole('alert').filter({ hasText: 'Stop forbidden by fixture' }).waitFor();
+ fixture.failTensorboardStop = false;
+ await dialog.getByRole('button', { name: 'Stop', exact: true }).click();
+ await dialog.waitFor({ state: 'hidden' });
+ await page.getByRole('button', { name: 'Start Tensorboard', exact: true }).waitFor();
+ assert.equal(await image.inputValue(), tensorboardImage);
+ assert.equal(await page.getByText('Stop forbidden by fixture', { exact: true }).count(), 0);
+ assert.equal(fixture.requests.filter((item) => item.method === 'POST').length, 2);
+ assert.equal(fixture.requests.filter((item) => item.method === 'DELETE').length, 2);
+ assert.ok(fixture.requests.some((item) => item.method === 'HEAD'));
+ });
+});
+
+test('artifact columns resize by pointer, persist after reload, and reset on ultrawide screens', async () => {
+ await withFixture({ namespace: 'team-a' }, async (page) => {
+ await page.setViewportSize({ width: 3440, height: 1263 });
+ await page.goto(`${origin}/#/artifacts`);
+ await ready(page, 16);
+ const table = page.getByRole('table', { name: 'Artifacts', exact: true });
+ const name = table.getByRole('columnheader', { name: 'Name', exact: true });
+ const before = await name.boundingBox();
+ const tableBounds = await table.boundingBox();
+ assert.ok(Math.abs(before.width / tableBounds.width - 0.28) < 0.01);
+ const handle = await table
+ .getByRole('separator', { name: 'Resize Name column', exact: true })
+ .boundingBox();
+ const x = handle.x + handle.width / 2;
+ const y = handle.y + handle.height / 2;
+ await page.mouse.move(x, y);
+ await page.mouse.down();
+ await page.mouse.move(x + 120, y, { steps: 6 });
+ await page.mouse.up();
+ await page.waitForFunction((expected) => {
+ const heading = document.querySelector('table[aria-label="Artifacts"] th[aria-label="Name"]');
+ return Math.abs(heading.getBoundingClientRect().width - expected) <= 2;
+ }, before.width + 120);
+ const resized = await name.boundingBox();
+ assert.ok(
+ Math.abs(resized.width - before.width - 120) <= 2,
+ `Name width before=${before.width}, after=${resized.width}`,
+ );
+ const shrinkHandle = await table
+ .getByRole('separator', { name: 'Resize Name column', exact: true })
+ .boundingBox();
+ const shrinkX = shrinkHandle.x + shrinkHandle.width / 2;
+ await page.mouse.move(shrinkX, y);
+ await page.mouse.down();
+ await page.mouse.move(shrinkX + 160 - resized.width, y, { steps: 8 });
+ await page.mouse.up();
+ await page.waitForFunction(
+ () =>
+ Math.abs(
+ document
+ .querySelector('table[aria-label="Artifacts"] th[aria-label="Name"]')
+ .getBoundingClientRect().width - 160,
+ ) <= 1,
+ );
+ const region = page.getByRole('region', { name: 'Artifacts table', exact: true });
+ assert.ok(
+ Math.abs((await table.boundingBox()).width - (await region.boundingBox()).width) <= 1,
+ 'shrinking a column must keep the table filled',
+ );
+ assert.ok(
+ Math.abs((await name.boundingBox()).width - 160) <= 1,
+ 'surplus must not stretch the resized column',
+ );
+ await page.reload();
+ await ready(page, 16);
+ assert.ok(Math.abs((await name.boundingBox()).width - 160) <= 1);
+ assert.ok(
+ Math.abs((await table.boundingBox()).width - (await region.boundingBox()).width) <= 1,
+ );
+ await page.setViewportSize({ width: 375, height: 812 });
+ assert.ok(Math.abs((await name.boundingBox()).width - 160) <= 1);
+ assert.ok(
+ await region.evaluate((element) => element.scrollWidth > element.clientWidth),
+ 'narrow layouts should scroll rather than shrink user widths',
+ );
+ await page.setViewportSize({ width: 3440, height: 1263 });
+ await page.getByRole('button', { name: 'Reset column widths', exact: true }).click();
+ await page
+ .getByRole('button', { name: 'Reset column widths', exact: true })
+ .waitFor({ state: 'hidden' });
+ await page.waitForFunction((expected) => {
+ const heading = document.querySelector('table[aria-label="Artifacts"] th[aria-label="Name"]');
+ return Math.abs(heading.getBoundingClientRect().width - expected) <= 1;
+ }, before.width);
+ assert.ok(
+ Math.abs((await name.boundingBox()).width - before.width) <= 1,
+ `reset width=${(await name.boundingBox()).width}; before=${before.width}; table=${(await table.boundingBox()).width}`,
+ );
+ await page.reload();
+ await ready(page, 16);
+ assert.ok(
+ Math.abs((await name.boundingBox()).width - before.width) <= 1,
+ `reset width=${(await name.boundingBox()).width}; before=${before.width}; table=${(await table.boundingBox()).width}`,
+ );
+ });
+});
diff --git a/frontend/scripts/ui-modernization-comparison.smoke.mjs b/frontend/scripts/ui-modernization-comparison.smoke.mjs
new file mode 100644
index 00000000000..8961a412dbb
--- /dev/null
+++ b/frontend/scripts/ui-modernization-comparison.smoke.mjs
@@ -0,0 +1,837 @@
+/*
+ * Copyright 2026 The Kubeflow Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Run after npm run build. Fixtures exercise real native-run comparison and viewers.
+// Optional candidate-only timings: KFP_SCALING_SAMPLES=3 KFP_SCALING_OUTPUT_DIR=/tmp/...
+// KFP_SOURCE_COMMIT=${query.key === 'alpha.html' ? 'Alpha' : 'Beta'} report
Details
+ {diagnostics}
+