mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(epg): stack programme dialog actions on phones
Below the 640px phone breakpoint the viewport caps the programme dialog,
and a long translated primary label ("Regarder depuis le début") no
longer fit beside Close. The footer had no wrap, and the dialog hides
overflow, so the label was clipped.
- At the phone breakpoint, the archive tools and the footer now stack one
full-width button per row, in DOM order.
- Buttons grow to fit their label, so a long label wraps inside its
button instead of being clipped.
- On desktop the footer can wrap again as a last resort.
The new Electron test opens a past programme in French at a 360px
viewport and measures both rows. It fails against the previous
stylesheet (the footer buttons are 44px narrower than the row).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
228ecf5c8b
commit
9325753d2d
3 files changed
+133
-2
No files matched your search
@@ -21,6 +21,8 @@ import {
|
||||
} from './portal-mock-fixtures';
|
||||
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
applyTheme,
|
||||
expectTextContrast,
|
||||
@@ -315,6 +317,112 @@ test('@epg @xtream @electron opens the programme dialog from a timeline block an
|
||||
}
|
||||
});
|
||||
|
||||
test('@epg @xtream @electron stacks the programme dialog actions on a phone', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
// French carries the longest primary label ("watch from start").
|
||||
const fr = JSON.parse(
|
||||
readFileSync(
|
||||
join(__dirname, '../../web/src/assets/i18n/fr.json'),
|
||||
'utf8'
|
||||
)
|
||||
) as {
|
||||
WORKSPACE: { SHELL: { RAIL_LIVE: string } };
|
||||
EPG: {
|
||||
PROGRAM_DIALOG: { SHOW_PROGRAM_DETAILS: string };
|
||||
TIMELINE: { WATCH_FROM_START: string };
|
||||
};
|
||||
};
|
||||
await resetMockServers(request, ['xtream']);
|
||||
const fixture = await fetchXtreamEpgFixture(request, epgCredentials);
|
||||
const app = await launchElectronApp(dataDir, { env: { TZ: 'UTC' } });
|
||||
|
||||
try {
|
||||
await app.mainWindow.route('https://test-streams.mux.dev/**', () => {
|
||||
// Keep the external demo request pending; guide data is local.
|
||||
});
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
name: 'Xtream Phone Dialog',
|
||||
username: epgCredentials.username,
|
||||
password: epgCredentials.password,
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
await openSettings(app.mainWindow);
|
||||
await app.mainWindow.getByTestId('select-language').click();
|
||||
await app.mainWindow.locator('mat-option[data-test-id="fr"]').click();
|
||||
await openSettingsSection(app.mainWindow, 'epg');
|
||||
await app.mainWindow.getByTestId('epg-view-mode-list').click();
|
||||
await saveSettings(app.mainWindow);
|
||||
|
||||
await openWorkspaceSection(
|
||||
app.mainWindow,
|
||||
fr.WORKSPACE.SHELL.RAIL_LIVE
|
||||
);
|
||||
await clickCategoryByNameExact(app.mainWindow, fixture.categoryName);
|
||||
await channelItemByTitle(app.mainWindow, fixture.stream.name ?? '')
|
||||
.first()
|
||||
.click();
|
||||
const guide = app.mainWindow.locator('app-epg-list-view');
|
||||
await expect(guide.locator('[data-when="past"]').first()).toBeVisible();
|
||||
|
||||
// The window cannot shrink below its desktop minimum, so emulate a
|
||||
// phone viewport below the 640px breakpoint.
|
||||
await app.mainWindow.setViewportSize({ width: 360, height: 800 });
|
||||
await guide
|
||||
.locator('[data-when="past"]')
|
||||
.first()
|
||||
.getByRole('button', {
|
||||
name: fr.EPG.PROGRAM_DIALOG.SHOW_PROGRAM_DETAILS,
|
||||
})
|
||||
.click();
|
||||
const dialog = app.mainWindow.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.locator('.epg-dialog__btn--primary')).toContainText(
|
||||
fr.EPG.TIMELINE.WATCH_FROM_START
|
||||
);
|
||||
|
||||
// Every action row stacks one full-width button per row in DOM
|
||||
// order, and no label spills out of its button.
|
||||
const rows = await dialog
|
||||
.locator('.epg-dialog__tools, .epg-dialog__actions')
|
||||
.evaluateAll((containers) =>
|
||||
containers.map((container) => {
|
||||
const buttons = Array.from(
|
||||
container.querySelectorAll<HTMLElement>('button')
|
||||
);
|
||||
return {
|
||||
width: (container as HTMLElement).clientWidth,
|
||||
buttons: buttons.map((button) => ({
|
||||
top: button.offsetTop,
|
||||
bottom: button.offsetTop + button.offsetHeight,
|
||||
width: button.offsetWidth,
|
||||
overflows:
|
||||
button.scrollWidth > button.clientWidth + 1,
|
||||
})),
|
||||
};
|
||||
})
|
||||
);
|
||||
expect(rows).toHaveLength(2);
|
||||
for (const row of rows) {
|
||||
expect(row.buttons.length).toBeGreaterThan(1);
|
||||
row.buttons.forEach((button, index) => {
|
||||
expect(Math.abs(button.width - row.width)).toBeLessThanOrEqual(
|
||||
1
|
||||
);
|
||||
expect(button.overflows).toBe(false);
|
||||
if (index > 0) {
|
||||
expect(button.top).toBeGreaterThanOrEqual(
|
||||
row.buttons[index - 1].bottom
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('@epg @stalker @theme @electron applies live themes to the shared Stalker guide', async ({
|
||||
dataDir,
|
||||
request,
|
||||
|
||||
@@ -671,7 +671,9 @@ Settings use the same system but are flatter than content-heavy views.
|
||||
button per row, in DOM order (dismiss on top, primary at the bottom). The
|
||||
settings unsaved-changes dialog is the reference;
|
||||
`settings-unsaved-dialog-layout.e2e.ts` in `web-e2e` measures it in six
|
||||
locales.
|
||||
locales. The programme dialog stacks its archive tools and footer the same
|
||||
way, letting a long label wrap inside its button; the Electron
|
||||
`epg-timeline-interaction.e2e.ts` measures it in French at 360px.
|
||||
- **Width.** A dialog with several openers is opened through one helper that
|
||||
owns its `MatDialogConfig`, so its width never depends on the entry point.
|
||||
`EpgProgrammeDialogService` opens the programme dialog at 540px from the
|
||||
|
||||
@@ -174,9 +174,11 @@
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* Dismiss first, primary last, both on the inline end. */
|
||||
/* Dismiss first, primary last, both on the inline end. Wrapping is only a
|
||||
last resort; both buttons fit one row at the 540px width. */
|
||||
.epg-dialog__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
@@ -222,6 +224,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Phone breakpoint: the viewport caps the dialog, so actions stack one per
|
||||
row at full width, in DOM order (dismiss above the primary). A long
|
||||
translated label wraps inside its button instead of being clipped. */
|
||||
@media (max-width: 640px) {
|
||||
.epg-dialog__tools,
|
||||
.epg-dialog__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.epg-dialog__btn {
|
||||
justify-content: center;
|
||||
height: auto;
|
||||
min-height: 40px;
|
||||
padding-block: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Let the dialog provide its own surface, border and shadow. Scoped to the
|
||||
panel class from EPG_PROGRAMME_DIALOG_CONFIG so no other dialog loses its
|
||||
surface while this one is open. */
|
||||
|
||||
Reference in new issue
Block a user