Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions frontend/src/pages/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,17 @@ function DeviceAuthStatus({
</p>
<CodeCountdown expiresAt={authorization.expiresAt} />
{authorization.verificationUri ? (
<p style={{ margin: '0.25rem 0 0.75rem' }}>
<a
<div style={{ margin: '0.25rem 0 0.75rem' }}>
<Button
href={authorization.verificationUri}
target="_blank"
rel="noopener"
attributes={{ target: '_blank', rel: 'noopener' }}
color="primary"
variant="outline"
size="small"
>
Open approval page
</a>
</p>
</Button>
</div>
) : null}
<p>Open the approval page and enter the code above to continue.</p>
{cancelButton}
Expand Down
73 changes: 73 additions & 0 deletions frontend/tests/editor-login-visual.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { test, expect } from '@playwright/test';

// editor.html defaults to OverallMode.full (real auth), so with no persisted
// token it lands on the "Not logged in yet?" screen instead of the demo editor.
// No backend mocking needed: useDeviceAuth only calls out when a token is in
// localStorage (see src/hooks/useDeviceAuth.ts hydrate effect), and there isn't one.
test('editor.html shows the login screen — visual regression', async ({ page }) => {
// Skip the onboarding carousel so the login screen renders directly.
await page.addInitScript(() => {
window.localStorage.setItem('hasCompletedOnboarding', 'true');
});

await page.goto('/editor.html');

await expect(page.getByText('Not logged in yet?')).toBeVisible();
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();

await expect(page).toHaveScreenshot('editor-login.png', {
fullPage: true,
});
});

test('editor.html device-auth screen shows a styled "Open approval page" button', async ({
page,
}) => {
// Freeze time so the live countdown (CodeCountdown, ticking via setInterval)
// renders a fixed "Code expires in 10:00" instead of a value that drifts with
// however long the test takes to run.
await page.clock.install({ time: new Date('2024-01-01T00:00:00Z') });

await page.addInitScript(() => {
window.localStorage.setItem('hasCompletedOnboarding', 'true');
});

// Stand in for the device-authorization endpoints (src/api/deviceAuth.ts) so
// clicking Login reaches the "polling" state without a real backend.
await page.route('**/auth/device/code', async (route) => {
await route.fulfill({
status: 200,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
device_code: 'mock-device-code',
user_code: 'ABCD-EFGH',
verification_uri: 'https://example.com/device',
expires_in: 600,
interval: 5,
}),
});
});
// Never resolves — keeps the UI in the "polling" state for the screenshot.
await page.route('**/auth/device/token', async (route) => {
await route.fulfill({
status: 400,
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ error: 'authorization_pending' }),
});
});

await page.goto('/editor.html');
await page.getByRole('button', { name: 'Login' }).click();

const approvalLink = page.getByRole('link', { name: 'Open approval page' });
await expect(approvalLink).toBeVisible();
await expect(approvalLink).toHaveAttribute(
'href',
'https://example.com/device',
);
await expect(page.getByText('Code expires in 10:00')).toBeVisible();

await expect(page).toHaveScreenshot('editor-login-device-code.png', {
fullPage: true,
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading