diff --git a/packages/apps/studio/client/components/issue-table.tsx b/packages/apps/studio/client/components/issue-table.tsx
index 8ef2aaba777..a6f3b1e3352 100644
--- a/packages/apps/studio/client/components/issue-table.tsx
+++ b/packages/apps/studio/client/components/issue-table.tsx
@@ -505,12 +505,15 @@ export function IssueTable({
- {label.name}
+
+ {label.name}
+
))}
{row.labels.length > 3 ? (
diff --git a/packages/apps/studio/e2e/mobile.test.ts b/packages/apps/studio/e2e/mobile.test.ts
index 59c61c3db7c..9e44f9e7ba9 100644
--- a/packages/apps/studio/e2e/mobile.test.ts
+++ b/packages/apps/studio/e2e/mobile.test.ts
@@ -149,14 +149,15 @@ test.describe('floating chat button at phone width', () => {
}
});
-test('deep issue tables fit narrow screens in both languages and modes and retain sorting', async ({
+test('deep issue tables retain label prefixes and sorting on phone and desktop in both languages and modes', async ({
page,
api,
}) => {
test.setTimeout(120_000);
const tag = `hierarchy-${Date.now()}`;
+ const labelName = `SECURITY: customer data export review ${tag}`;
const label = await api.post<{ id: string }>('projects/labels', {
- name: `Long label 标签 ${tag}`,
+ name: labelName,
color: 'purple',
});
const issueIds: string[] = [];
@@ -206,7 +207,7 @@ test('deep issue tables fit narrow screens in both languages and modes and retai
});
// The fixture signs in through the API; navigate before accessing origin-scoped storage.
await open(page, `/issues?view=list&q=${tag}`);
- for (const width of [375, 390]) {
+ for (const width of [375, 390, 1024, 1440]) {
await page.setViewportSize({ width, height: 844 });
for (const locale of ['zh-CN', 'en-US']) {
for (const mode of ['light', 'dark']) {
@@ -222,6 +223,37 @@ test('deep issue tables fit narrow screens in both languages and modes and retai
identifiers,
);
await expect(table.locator('[data-deploy-mark]').first()).toBeVisible();
+ const badges = table.getByTitle(labelName, { exact: true });
+ await expect(badges).toHaveCount(9);
+ // Check the visible prefix and real text ellipsis, not only page overflow.
+ const labelLayout = await page.evaluate<
+ { prefixVisible: boolean; ellipsis: boolean; clipped: boolean }[]
+ >(`(() => {
+ const badges = Array.from(document.querySelectorAll('table [data-slot="badge"]'))
+ .filter((badge) => badge.title === ${JSON.stringify(labelName)});
+ return badges.map((badge) => {
+ const text = badge.firstElementChild;
+ if (!text || !text.firstChild) return { prefixVisible: false, ellipsis: false, clipped: false };
+ const range = document.createRange();
+ range.setStart(text.firstChild, 0);
+ range.setEnd(text.firstChild, 'SECURITY'.length);
+ const prefix = range.getBoundingClientRect();
+ const bounds = text.getBoundingClientRect();
+ const badgeBounds = badge.getBoundingClientRect();
+ const style = getComputedStyle(text);
+ return {
+ prefixVisible: prefix.left >= bounds.left - 1 && prefix.right <= bounds.right + 1 && bounds.left >= badgeBounds.left && bounds.right <= badgeBounds.right,
+ ellipsis: style.textOverflow === 'ellipsis' && style.overflowX === 'hidden' && style.whiteSpace === 'nowrap',
+ clipped: text.scrollWidth > text.clientWidth,
+ };
+ });
+ })()`);
+ expect(labelLayout).toHaveLength(9);
+ expect(labelLayout.every((label) => label.prefixVisible)).toBe(true);
+ expect(labelLayout.every((label) => label.ellipsis)).toBe(true);
+ if (width <= 1024) {
+ expect(labelLayout.some((label) => label.clipped)).toBe(true);
+ }
expect(await horizontalOverflow(page)).toBeLessThanOrEqual(0);
// A page may fit while the generic Table wrapper still scrolls horizontally.
const overflow = await page.evaluate(`(() => {
@@ -234,13 +266,17 @@ test('deep issue tables fit narrow screens in both languages and modes and retai
})()`);
expect(Math.max(...overflow)).toBeLessThanOrEqual(0);
const priority = locale === 'zh-CN' ? '优先级' : 'Priority';
- await page.getByRole('button', { name: priority, exact: true }).click();
+ await page
+ .getByRole('button', { name: priority, exact: true })
+ .filter({ visible: true })
+ .click();
await expect(page).toHaveURL(/sort=priority/);
await page
.getByRole('button', {
name: locale === 'zh-CN' ? '更新时间' : 'Updated',
exact: true,
})
+ .filter({ visible: true })
.click();
await expect
.poll(() => new URL(page.url()).searchParams.get('sort'))