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
1 change: 1 addition & 0 deletions addon/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export {
export { default as buildWaiter, _resetWaiterNames } from './build-waiter.ts';
export { default as waitForPromise } from './wait-for-promise.ts';
export { default as waitFor } from './wait-for.ts';
export { waitForFetch } from './wait-for-fetch.ts';
22 changes: 22 additions & 0 deletions addon/src/wait-for-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { default as waitForPromise } from './wait-for-promise.ts';

export async function waitForFetch(fetchPromise: ReturnType<typeof fetch>) {
const response = await waitForPromise(fetchPromise);

return new Proxy(response, {
get(target, prop, receiver) {
const original = Reflect.get(target, prop, receiver);

if (
typeof prop === 'string' &&
['json', 'text', 'arrayBuffer', 'blob', 'formData', 'bytes'].includes(prop)
) {
return (...args: unknown[]) => {
return waitForPromise(original.call(target, ...args));
};
}

return original;
},
});
}
2 changes: 1 addition & 1 deletion test-apps/base-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"webpack": "^5.96.1"
},
"engines": {
"node": "16.* || >= 18"
"node": ">= 18"
},
"ember": {
"edition": "octane"
Expand Down
188 changes: 188 additions & 0 deletions test-apps/base-tests/tests/unit/wait-for-fetch-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { module, test } from 'qunit';
import { settled } from '@ember/test-helpers';
import { waitForFetch, getPendingWaiterState } from '@ember/test-waiters';

function expectWaiters(assert: Assert, expected: number) {
const result = getPendingWaiterState().pending;
assert.strictEqual(
result,
expected,
`Expecting ${expected} pending waiter(s)`,
);
}

function createFormData(obj: any) {
const data = new FormData();

for (const [k, v] of Object.entries(obj)) {
// SAFETY: just for testing, we can be a bit looser
data.append(k, v as string);
}

return data;
}

const supported = [
{
method: 'json',
input: new Response('{ "hi": "there" }'),
parsed: { hi: 'there' },
},
{ method: 'text', input: new Response('text'), parsed: 'text' },
{
method: 'arrayBuffer',
input: new Response(new ArrayBuffer(8)),
parsed: new ArrayBuffer(8),
},
{
method: 'blob',
input: new Response(new Blob(['hi'])),
parsed: new Blob(['hi']),
},
{
method: 'formData',
input: new Response(createFormData({ x: 1 })),
parsed: createFormData({ x: 1 }),
},
{
method: 'bytes',
input: new Response(new Uint8Array([92])),
parsed: new Uint8Array([92]),
},
] as const;

module('waitForFetch', function () {
for (const scenario of supported) {
test(`[${scenario.method}] minimal`, async function (assert) {
const fetchish = new Promise<Response>((resolve) =>
resolve(scenario.input),
);
const requestPromise = waitForFetch(fetchish);

expectWaiters(assert, 1);
const response = await requestPromise;

expectWaiters(assert, 0);

const responsePromise = (response as any)[scenario.method]();
expectWaiters(assert, 1);

const verify = await responsePromise;
expectWaiters(assert, 0);

assert.deepEqual(
verify,
scenario.parsed,
// eslint-disable-next-line @typescript-eslint/no-base-to-string
`Expected: ${scenario.parsed.toString()}`,
);
});
}

test('timing: empty response', async function (assert) {
function stepPromise<T>(
assert: Assert,
label: string,
promise: Promise<T>,
) {
return promise
.then((result) => {
assert.step(`${label}:then`);
return result;
})
.catch((e) => {
assert.step(`${label}:catch`);
return e;
});
}

function fetchLike(assert: Assert) {
const handlers = {} as {
resolve: <T>(r: T) => unknown;
reject: (e: unknown) => unknown;
};
const original = new Promise((resolve, reject) => {
assert.step('fetch:start');
handlers.resolve = resolve;
handlers.reject = reject;
});

const promise = stepPromise(assert, 'fetch', original);

return {
promise,
resolve(value: unknown) {
return handlers.resolve(value);
},
reject(e: unknown) {
return handlers.reject(e);
},
};
}

function proxiedResponse(assert: Assert, response: Response) {
return new Proxy(response, {
get(target, prop, receiver) {
const original = Reflect.get(target, prop, receiver);

if (
typeof prop === 'string' &&
[
'json',
'text',
'arrayBuffer',
'blob',
'formData',
'bytes',
].includes(prop)
) {
return (...args: unknown[]) => {
const label = `response:${prop}`;
// not part of waiting, but this tells us
// we proxied correctly
assert.step(label);
const promise = original.call(target, ...args);

return stepPromise(assert, label, promise);
};
}

return original;
},
});
}

const f = fetchLike(assert);

expectWaiters(assert, 0);
const requestPromise = waitForFetch(f.promise);

assert.verifySteps(['fetch:start']);
expectWaiters(assert, 1);

f.resolve(proxiedResponse(assert, new Response()));
assert.verifySteps([]);
expectWaiters(assert, 1);

await settled();
assert.verifySteps(['fetch:then'], `Nothing on the response accessed`);
assert.verifySteps([]);
expectWaiters(assert, 0);

const response = await requestPromise;
assert.verifySteps([]);
expectWaiters(assert, 0);

const responsePromise = response.text();
assert.verifySteps(['response:text']);
expectWaiters(assert, 1);

await settled();
assert.verifySteps(['response:text:then']);
expectWaiters(assert, 0);

await responsePromise;
assert.verifySteps([]);
expectWaiters(assert, 0);
});
});