fix web review validation and clipboard tests

This commit is contained in:
2026-07-19 20:50:42 +08:00
parent ec59dee6b9
commit 884148b88f
5 changed files with 69 additions and 3 deletions

View 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`

View File

@@ -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('/');

View File

@@ -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('@');

View File

@@ -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.'],

View File

@@ -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 } });