fix web review validation and clipboard tests
This commit is contained in:
12
.superpowers/sdd/final-fix-4-report.md
Normal file
12
.superpowers/sdd/final-fix-4-report.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Final Fix 4 Report
|
||||
|
||||
- Rejected raw server values containing backslashes and malformed HTTP-like prefixes before protocol-less normalization.
|
||||
- Added unit and Playwright coverage for malformed slash and backslash server inputs.
|
||||
- Added CustomLinkChannel tests for deferred clipboard success, rejection feedback, and unavailable clipboard feedback.
|
||||
|
||||
Verification passed:
|
||||
|
||||
- `npm test -- --run` (8 files, 48 tests)
|
||||
- `npm run build`
|
||||
- `npx playwright test` (11 tests)
|
||||
- `git diff --check`
|
||||
@@ -25,7 +25,17 @@ test('login and project channel workbench flow', async ({ page }) => {
|
||||
await expect(page.getByText('Open external channel')).toBeVisible();
|
||||
});
|
||||
|
||||
for (const server of ['https://user:secret@example.com', 'http:////example.com', 'http://example..com', 'http://-bad.com']) {
|
||||
for (const server of [
|
||||
'https://user:secret@example.com',
|
||||
'http:////example.com',
|
||||
'http//example.com',
|
||||
'https//example.com',
|
||||
'http:/example.com',
|
||||
'https:/example.com',
|
||||
'http://example.com\\path',
|
||||
'http://example..com',
|
||||
'http://-bad.com',
|
||||
]) {
|
||||
test(`rejects malformed server address: ${server}`, async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
return { error: 'Enter a server address.' };
|
||||
}
|
||||
|
||||
if (hasWhitespace(value) || hasUnsupportedProtocol(value)) {
|
||||
if (hasWhitespace(value) || value.includes('\\') || hasMalformedHttpProtocol(value) || hasUnsupportedProtocol(value)) {
|
||||
return { error: 'Enter a valid HTTP or HTTPS server address.' };
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
return /^[a-z][a-z\d+.-]*:(?!\d)/i.test(value);
|
||||
}
|
||||
|
||||
function hasMalformedHttpProtocol(value: string): boolean {
|
||||
return /^https?:\/(?!\/)/i.test(value) || /^https?\/{2}(?!\/)/i.test(value);
|
||||
}
|
||||
|
||||
function hasCredentials(value: string): boolean {
|
||||
const authority = value.replace(/^https?:\/\//i, '').split(/[/?#]/, 1)[0];
|
||||
return authority.includes('@');
|
||||
|
||||
@@ -76,6 +76,12 @@ describe('ServerLogin', () => {
|
||||
['http://', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['ftp://example.com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['http:////example.com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['http//example.com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['https//example.com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['http:/example.com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['https:/example.com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['http://example.com\\path', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['example.com\\path', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['http://example..com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['http://-bad.com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
['http://bad-.com', 'Enter a valid HTTP or HTTPS server address.'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import CustomLinkChannel from './CustomLinkChannel.svelte';
|
||||
|
||||
const channel = {
|
||||
@@ -14,6 +14,13 @@ const channel = {
|
||||
};
|
||||
const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
|
||||
|
||||
function setClipboard(writeText: Clipboard['writeText']) {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText } as Clipboard,
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
if (originalClipboard) {
|
||||
@@ -24,6 +31,33 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('CustomLinkChannel', () => {
|
||||
it('shows copy success only after clipboard writing resolves', async () => {
|
||||
let resolveWrite: (() => void) | undefined;
|
||||
const writeText = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveWrite = resolve;
|
||||
}),
|
||||
);
|
||||
setClipboard(writeText);
|
||||
render(CustomLinkChannel, { props: { channel } });
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Copy URL' }));
|
||||
|
||||
expect(screen.queryByText('URL copied.')).not.toBeInTheDocument();
|
||||
resolveWrite?.();
|
||||
await waitFor(() => expect(screen.getByText('URL copied.')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('reports a copy failure when clipboard writing rejects', async () => {
|
||||
setClipboard(vi.fn().mockRejectedValue(new Error('Permission denied')));
|
||||
render(CustomLinkChannel, { props: { channel } });
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Copy URL' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Unable to copy URL.')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('reports unavailable clipboard access without showing URL copied', async () => {
|
||||
Reflect.deleteProperty(navigator, 'clipboard');
|
||||
render(CustomLinkChannel, { props: { channel } });
|
||||
|
||||
Reference in New Issue
Block a user