deno.land / x / ky@v0.31.3 / test / browser.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522import test, {ExecutionContext} from 'ava';import busboy from 'busboy';import express from 'express';import {Page} from 'playwright-chromium';import ky from '../source/index.js';import {createHttpTestServer, HttpServerOptions} from './helpers/create-http-test-server.js';import {parseRawBody} from './helpers/parse-body.js';import {withPage} from './helpers/with-page.js';
declare global { interface Window { ky: typeof ky; }}
const DIST_DIR = new URL('../distribution', import.meta.url).toString();const createEsmTestServer = async (options?: HttpServerOptions) => { const server = await createHttpTestServer(options); server.use('/distribution', express.static(DIST_DIR.replace(/^file:\/\//, ''))); return server;};
const KY_SCRIPT = { type: 'module', content: ` import ky from '/distribution/index.js'; globalThis.ky = ky; `,};const addKyScriptToPage = async (page: Page) => { await page.addScriptTag(KY_SCRIPT); await page.waitForFunction(() => typeof window.ky === 'function');};
test('prefixUrl option', withPage, async (t: ExecutionContext, page: Page) => { const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end('zebra'); });
server.get('/api/unicorn', (_request, response) => { response.end('rainbow'); });
await page.goto(server.url); await addKyScriptToPage(page);
await t.throwsAsync( page.evaluate(async () => window.ky('/foo', {prefixUrl: '/'})), {message: /`input` must not begin with a slash when using `prefixUrl`/}, );
const results = await page.evaluate(async (url: string) => Promise.all([ window.ky(`${url}/api/unicorn`).text(), // @ts-expect-error unsupported {prefixUrl: null} type window.ky(`${url}/api/unicorn`, {prefixUrl: null}).text(), window.ky('api/unicorn', {prefixUrl: url}).text(), window.ky('api/unicorn', {prefixUrl: `${url}/`}).text(), ]), server.url);
t.deepEqual(results, ['rainbow', 'rainbow', 'rainbow', 'rainbow']);
await server.close();});
test('aborting a request', withPage, async (t: ExecutionContext, page: Page) => { const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end('meow'); });
server.get('/test', (_request, response) => { setTimeout(() => { response.end('ok'); }, 500); });
await page.goto(server.url); await addKyScriptToPage(page);
const error = await page.evaluate(async (url: string) => { const controller = new AbortController(); const request = window.ky(`${url}/test`, {signal: controller.signal}).text(); controller.abort(); return request.catch(error_ => error_.toString()); }, server.url); t.is(error, 'AbortError: Failed to execute \'fetch\' on \'Window\': The user aborted a request.');
await server.close();});
test('should copy origin response info when using `onDownloadProgress`', withPage, async (t: ExecutionContext, page: Page) => { const json = {hello: 'world'}; const status = 202; const statusText = 'Accepted'; const server = await createEsmTestServer(); server.get('/', (_request, response) => { response.end('meow'); });
server.get('/test', (_request, response) => { setTimeout(() => { response.statusMessage = statusText; response.status(status).header('X-ky-Header', 'ky').json(json); }, 500); }); await page.goto(server.url); await addKyScriptToPage(page); const data = await page.evaluate(async (url: string) => { // eslint-disable-next-line @typescript-eslint/no-empty-function const request = window.ky.get(`${url}/test`, {onDownloadProgress() {}}).then(async v => ({ headers: v.headers.get('X-ky-Header'), status: v.status, statusText: v.statusText, data: await v.json(), })); return request; }, server.url);
t.deepEqual(data, { status, headers: 'ky', statusText, data: json, }); await server.close();});
test('should not copy response body with 204 status code when using `onDownloadProgress` ', withPage, async (t: ExecutionContext, page: Page) => { const status = 204; const statusText = 'No content'; const server = await createEsmTestServer(); server.get('/', (_request, response) => { response.end('meow'); });
server.get('/test', (_request, response) => { setTimeout(() => { response.statusMessage = statusText; response.status(status).header('X-ky-Header', 'ky').end(null); }, 500); }); await page.goto(server.url); await addKyScriptToPage(page); const data = await page.evaluate(async (url: string) => { const progress: any = []; let totalBytes = 0; const response = await window.ky.get(`${url}/test`, { onDownloadProgress(progressEvent) { progress.push(progressEvent); }, }).then(async v => { totalBytes = Number(v.headers.get('content-length')) || 0; return ({ headers: v.headers.get('X-ky-Header'), status: v.status, statusText: v.statusText, }); }); return { response, progress, totalBytes, }; }, server.url);
t.deepEqual(data.response, { status, headers: 'ky', statusText, }); t.deepEqual(data.progress, [{ percent: 1, totalBytes: data.totalBytes, transferredBytes: 0, }]);
await server.close();});
test('aborting a request with onDonwloadProgress', withPage, async (t: ExecutionContext, page: Page) => { const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end('meow'); });
server.get('/test', (_request, response) => { response.writeHead(200, { 'content-length': '4', });
response.write('me'); setTimeout(() => { response.end('ow'); }, 1000); });
await page.goto(server.url); await addKyScriptToPage(page);
const error = await page.evaluate(async (url: string) => { const controller = new AbortController(); // eslint-disable-next-line @typescript-eslint/no-empty-function const request = window.ky(`${url}/test`, {signal: controller.signal, onDownloadProgress() {}}).text(); setTimeout(() => { controller.abort(); }, 500); return request.catch(error_ => error_.toString()); }, server.url); // This should be an AbortError like in the 'aborting a request' test, but there is a bug in Chromium t.is(error, 'TypeError: Failed to fetch');
await server.close();});
test( 'throws TimeoutError even though it does not support AbortController', withPage, async (t: ExecutionContext, page: Page) => { const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end(); });
server.get('/slow', (_request, response) => { setTimeout(() => { response.end('ok'); }, 1000); });
await page.goto(server.url); await page.addScriptTag({content: 'window.AbortController = undefined;\n'}); await addKyScriptToPage(page);
const error = await page.evaluate(async (url: string) => { const request = window.ky(`${url}/slow`, {timeout: 500}).text(); return request.catch(error_ => ({ message: error_.toString(), request: {url: error_.request.url}, })); }, server.url);
if (typeof error !== 'object') { throw new TypeError('Expected to have an object error'); }
t.is(error.message, 'TimeoutError: Request timed out'); t.is(error.request.url, `${server.url}/slow`);
await server.close(); },);
test('onDownloadProgress works', withPage, async (t: ExecutionContext, page: Page) => { const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.writeHead(200, { 'content-length': '4', });
response.write('me'); setTimeout(() => { response.end('ow'); }, 1000); });
await page.goto(server.url); await addKyScriptToPage(page);
const result = await page.evaluate(async (url: string) => { // `new TextDecoder('utf-8').decode` hangs up? const decodeUtf8 = (array: Uint8Array) => String.fromCodePoint(...array);
const data: any[] = []; const text = await window .ky(url, { onDownloadProgress(progress, chunk) { const stringifiedChunk = decodeUtf8(chunk); data.push([progress, stringifiedChunk]); }, }) .text();
return {data, text}; }, server.url);
t.deepEqual(result.data, [ [{percent: 0, transferredBytes: 0, totalBytes: 4}, ''], [{percent: 0.5, transferredBytes: 2, totalBytes: 4}, 'me'], [{percent: 1, transferredBytes: 4, totalBytes: 4}, 'ow'], ]); t.is(result.text, 'meow');
await server.close();});
test('throws if onDownloadProgress is not a function', withPage, async (t: ExecutionContext, page: Page) => { const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end(); });
await page.goto(server.url); await addKyScriptToPage(page);
const error = await page.evaluate(async (url: string) => { // @ts-expect-error const request = window.ky(url, {onDownloadProgress: 1}).text(); return request.catch(error_ => error_.toString()); }, server.url); t.is(error, 'TypeError: The `onDownloadProgress` option must be a function');
await server.close();});
test('throws if does not support ReadableStream', withPage, async (t: ExecutionContext, page: Page) => { const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end(); });
await page.goto(server.url); await page.addScriptTag({content: 'window.ReadableStream = undefined;\n'}); await addKyScriptToPage(page);
const error = await page.evaluate(async (url: string) => { // eslint-disable-next-line @typescript-eslint/no-empty-function const request = window.ky(url, {onDownloadProgress() {}}).text(); return request.catch(error_ => error_.toString()); }, server.url); t.is(error, 'Error: Streams are not supported in your environment. `ReadableStream` is missing.');
await server.close();});
test('FormData with searchParams', withPage, async (t: ExecutionContext, page: Page) => { t.plan(3);
const server = await createEsmTestServer({bodyParser: false});
server.get('/', (_request, response) => { response.end(); });
server.post('/', async (request, response) => { const requestBody = await parseRawBody(request); const contentType = request.headers['content-type']; const boundary = contentType!.split('boundary=')[1];
t.truthy(requestBody.includes(boundary!)); t.regex(requestBody, /bubblegum pie/); t.deepEqual(request.query, {foo: '1'}); response.end(); });
await page.goto(server.url); await addKyScriptToPage(page);
await page.evaluate(async (url: string) => { const formData = new window.FormData(); formData.append('file', new window.File(['bubblegum pie'], 'my-file')); return window.ky(url, { method: 'post', searchParams: 'foo=1', body: formData, }); }, server.url);
await server.close();});
test('FormData with searchParams ("multipart/form-data" parser)', withPage, async (t: ExecutionContext, page: Page) => { t.plan(3);
const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end(); });
server.post('/', async (request, response) => { const [body, error] = await new Promise(resolve => { // @ts-expect-error const busboyInstance = busboy({headers: request.headers});
busboyInstance.on('error', (error: Error) => { resolve([null, error]); });
// eslint-disable-next-line max-params busboyInstance.on('file', async (fieldname, file, filename, encoding, mimetype) => { let fileContent = ''; try { for await (const chunk of file) { fileContent += chunk; // eslint-disable-line @typescript-eslint/restrict-plus-operands }
resolve([{fieldname, filename, encoding, mimetype, fileContent}, undefined]); } catch (error_: unknown) { resolve([null, error_]); } });
busboyInstance.on('finish', () => { response.writeHead(303, {Connection: 'close', Location: '/'}); response.end(); });
setTimeout(() => { resolve([null, new Error('Timeout')]); }, 3000);
request.pipe(busboyInstance); });
t.falsy(error); t.deepEqual(request.query, {foo: '1'}); t.deepEqual(body, { fieldname: 'file', filename: 'my-file', encoding: '7bit', mimetype: 'text/plain', fileContent: 'bubblegum pie', }); });
await page.goto(server.url); await addKyScriptToPage(page);
await page.evaluate(async url => { const formData = new window.FormData();
formData.append('file', new window.File(['bubblegum pie'], 'my-file', {type: 'text/plain'}));
return window.ky(url, { method: 'post', searchParams: 'foo=1', body: formData, }); }, server.url);
await server.close();});
test( 'headers are preserved when input is a Request and there are searchParams in the options', withPage, async (t: ExecutionContext, page: Page) => { t.plan(2);
const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end(); });
server.get('/test', (request, response) => { t.is(request.headers['content-type'], 'text/css'); t.deepEqual(request.query, {foo: '1'}); response.end(); });
await page.goto(server.url); await addKyScriptToPage(page);
await page.evaluate(async (url: string) => { const request = new window.Request(`${url}/test`, { headers: {'content-type': 'text/css'}, });
return window .ky(request, { searchParams: 'foo=1', }) .text(); }, server.url);
await server.close(); },);
test('retry with body', withPage, async (t: ExecutionContext, page: Page) => { t.plan(4);
let requestCount = 0;
const server = await createEsmTestServer();
server.get('/', (_request, response) => { response.end('zebra'); });
server.put('/test', async (request, response) => { requestCount++; t.is(request.body, 'foo'); response.sendStatus(502); });
await page.goto(server.url); await addKyScriptToPage(page);
await t.throwsAsync( page.evaluate(async (url: string) => window.ky(`${url}/test`, { body: 'foo', method: 'PUT', retry: 2, }), server.url), {message: /HTTPError: Request failed with status code 502 Bad Gateway/}, );
t.is(requestCount, 2);
await server.close();});
Version Info