Skip to content

Commit

Permalink
Merge branch 'main' into allow-ember-test-helper-v4
Browse files Browse the repository at this point in the history
  • Loading branch information
mkszepp authored Sep 1, 2024
2 parents 3c9c5af + bd5d2ba commit 1be0954
Show file tree
Hide file tree
Showing 18 changed files with 427 additions and 244 deletions.
6 changes: 5 additions & 1 deletion packages/build-config/src/-private/utils/get-env.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
export function getEnv() {
const { EMBER_ENV, IS_TESTING, EMBER_CLI_TEST_COMMAND, NODE_ENV } = process.env;
const { EMBER_ENV, IS_TESTING, EMBER_CLI_TEST_COMMAND, NODE_ENV, CI, IS_RECORDING } = process.env;
const PRODUCTION = EMBER_ENV === 'production' || (!EMBER_ENV && NODE_ENV === 'production');
const DEBUG = !PRODUCTION;
const TESTING = DEBUG || Boolean(EMBER_ENV === 'test' || IS_TESTING || EMBER_CLI_TEST_COMMAND);
const SHOULD_RECORD = Boolean(!CI || IS_RECORDING);

return {
TESTING,
PRODUCTION,
DEBUG,
IS_RECORDING: Boolean(IS_RECORDING),
IS_CI: Boolean(CI),
SHOULD_RECORD,
};
}
3 changes: 3 additions & 0 deletions packages/build-config/src/babel-macros.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export function macros() {
TESTING: true,
PRODUCTION: true,
DEBUG: true,
IS_RECORDING: true,
IS_CI: true,
SHOULD_RECORD: true,
},
},
'@warp-drive/build-config/env',
Expand Down
3 changes: 3 additions & 0 deletions packages/build-config/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export const DEBUG: boolean = true;
export const PRODUCTION: boolean = true;
export const TESTING: boolean = true;
export const IS_RECORDING: boolean = true;
export const IS_CI: boolean = true;
export const SHOULD_RECORD: boolean = true;
66 changes: 60 additions & 6 deletions packages/ember/src/-private/request.gts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ function notNull<T>(x: T | null) {
const not = (x: unknown) => !x;
// default to 30 seconds unavailable before we refresh
const DEFAULT_DEADLINE = 30_000;
const IdleBlockMissingError = new Error(
'No idle block provided for <Request> component, and no query or request was provided.'
);

let consume = service;
if (macroCondition(moduleExists('ember-provide-consume-context'))) {
Expand Down Expand Up @@ -63,6 +66,7 @@ interface RequestSignature<T, RT> {
autorefreshBehavior?: 'refresh' | 'reload' | 'policy';
};
Blocks: {
idle: [];
loading: [state: RequestLoadingState];
cancelled: [
error: StructuredErrorDocument,
Expand Down Expand Up @@ -145,6 +149,7 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
declare intervalStart: number | null;
declare nextInterval: number | null;
declare invalidated: boolean;
declare isUpdating: boolean;

/**
* The event listener for network status changes,
Expand Down Expand Up @@ -190,8 +195,27 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
this.nextInterval = null;

this.installListeners();
this.updateSubscriptions();
void this.scheduleInterval();
void this.beginPolling();
}

async beginPolling() {
// await the initial request
try {
await this.request;
} catch {
// ignore errors here, we just want to wait for the request to finish
} finally {
if (!this.isDestroyed) {
void this.scheduleInterval();
}
}
}

@cached
get isIdle() {
const { request, query } = this.args;

return Boolean(!request && !query);
}

@cached
Expand Down Expand Up @@ -263,7 +287,10 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
}

updateSubscriptions() {
const requestId = this.request.lid;
if (this.isIdle) {
return;
}
const requestId = this._request.lid;

// if we're already subscribed to this request, we don't need to do anything
if (this._subscribedTo === requestId) {
Expand All @@ -275,9 +302,15 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {

// if we have a request, we need to subscribe to it
if (requestId) {
this._subscribedTo = requestId;
this._subscription = this.store.notifications.subscribe(
requestId,
(_id: StableDocumentIdentifier, op: 'invalidated' | 'state' | 'added' | 'updated' | 'removed') => {
// ignore subscription events that occur while our own component's request
// is ocurring
if (this.isUpdating) {
return;
}
switch (op) {
case 'invalidated': {
// if we're subscribed to invalidations, we need to update
Expand Down Expand Up @@ -366,6 +399,9 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
* @internal
*/
maybeUpdate(mode?: 'reload' | 'refresh' | 'policy' | 'invalidated', silent?: boolean): void {
if (this.isIdle) {
return;
}
const canAttempt = Boolean(this.isOnline && !this.isHidden && (mode || this.autorefreshTypes.size));

if (!canAttempt) {
Expand All @@ -391,7 +427,7 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
const { autorefreshThreshold } = this.args;

if (intervalStart && typeof autorefreshThreshold === 'number' && autorefreshThreshold > 0) {
shouldAttempt = Boolean(Date.now() - intervalStart > autorefreshThreshold);
shouldAttempt = Boolean(Date.now() - intervalStart >= autorefreshThreshold);
}
}

Expand Down Expand Up @@ -424,13 +460,20 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
!request.store || request.store === this.store
);

this.isUpdating = true;
this._latestRequest = wasStoreRequest ? this.store.request(request) : this.store.requestManager.request(request);

if (val !== 'refresh') {
this._localRequest = this._latestRequest;
}

void this.scheduleInterval();
void this._latestRequest.finally(() => {
this.isUpdating = false;
});
} else {
// TODO probably want this
// void this.scheduleInterval();
}
}

Expand Down Expand Up @@ -501,7 +544,7 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
}

@cached
get request(): Future<RT> {
get _request(): Future<RT> {
const { request, query } = this.args;
assert(`Cannot use both @request and @query args with the <Request> component`, !request || !query);
const { _localRequest, _originalRequest, _originalQuery } = this;
Expand All @@ -522,6 +565,13 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
return this.store.request<RT, T>(query);
}

@cached
get request(): Future<RT> {
const request = this._request;
this.updateSubscriptions();
return request;
}

get store(): Store {
const store = this.args.store || this._store;
assert(
Expand All @@ -542,7 +592,11 @@ export class Request<T, RT> extends Component<RequestSignature<T, RT>> {
}

<template>
{{#if this.reqState.isLoading}}
{{#if (and this.isIdle (has-block "idle"))}}
{{yield to="idle"}}
{{else if this.isIdle}}
<Throw @error={{IdleBlockMissingError}} />
{{else if this.reqState.isLoading}}
{{yield this.reqState.loadingState to="loading"}}
{{else if (and this.reqState.isCancelled (has-block "cancelled"))}}
{{yield (notNull this.reqState.error) this.errorFeatures to="cancelled"}}
Expand Down
176 changes: 98 additions & 78 deletions packages/holodeck/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,96 +147,116 @@ function replayRequest(context, cacheKey) {

function createTestHandler(projectRoot) {
const TestHandler = async (context) => {
const { req } = context;

const testId = req.query('__xTestId');
const testRequestNumber = req.query('__xTestRequestNumber');
const niceUrl = getNiceUrl(req.url);
try {
const { req } = context;

const testId = req.query('__xTestId');
const testRequestNumber = req.query('__xTestRequestNumber');
const niceUrl = getNiceUrl(req.url);

if (!testId) {
context.header('Content-Type', 'application/vnd.api+json');
context.status(400);
return context.body(
JSON.stringify({
errors: [
{
status: '400',
code: 'MISSING_X_TEST_ID_HEADER',
title: 'Request to the http mock server is missing the `X-Test-Id` header',
detail:
"The `X-Test-Id` header is used to identify the test that is making the request to the mock server. This is used to ensure that the mock server is only used for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
source: { header: 'X-Test-Id' },
},
],
})
);
}

if (!testId) {
context.header('Content-Type', 'application/vnd.api+json');
context.status(400);
return context.body(
JSON.stringify({
errors: [
{
status: '400',
code: 'MISSING_X_TEST_ID_HEADER',
title: 'Request to the http mock server is missing the `X-Test-Id` header',
detail:
"The `X-Test-Id` header is used to identify the test that is making the request to the mock server. This is used to ensure that the mock server is only used for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
source: { header: 'X-Test-Id' },
},
],
})
);
}
if (!testRequestNumber) {
context.header('Content-Type', 'application/vnd.api+json');
context.status(400);
return context.body(
JSON.stringify({
errors: [
{
status: '400',
code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',
title: 'Request to the http mock server is missing the `X-Test-Request-Number` header',
detail:
"The `X-Test-Request-Number` header is used to identify the request number for the current test. This is used to ensure that the mock server response is deterministic for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
source: { header: 'X-Test-Request-Number' },
},
],
})
);
}

if (!testRequestNumber) {
if (req.method === 'POST' || niceUrl === '__record') {
const payload = await req.json();
const { url, headers, method, status, statusText, body, response } = payload;
const cacheKey = generateFilepath({
projectRoot,
testId,
url,
method,
body: body ? JSON.stringify(body) : null,
testRequestNumber,
});
// allow Content-Type to be overridden
headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
// We always compress and chunk the response
headers['Content-Encoding'] = 'br';
// we don't cache since tests will often reuse similar urls for different payload
headers['Cache-Control'] = 'no-store';

const cacheDir = generateFileDir({
projectRoot,
testId,
url,
method,
testRequestNumber,
});

fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(
`${cacheKey}.meta.json`,
JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2)
);
fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response)));
context.status(204);
return context.body(null);
} else {
const body = await req.text();
const cacheKey = generateFilepath({
projectRoot,
testId,
url: niceUrl,
method: req.method,
body,
testRequestNumber,
});
return replayRequest(context, cacheKey);
}
} catch (e) {
if (e instanceof HTTPException) {
throw e;
}
context.header('Content-Type', 'application/vnd.api+json');
context.status(400);
context.status(500);
return context.body(
JSON.stringify({
errors: [
{
status: '400',
code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',
title: 'Request to the http mock server is missing the `X-Test-Request-Number` header',
detail:
"The `X-Test-Request-Number` header is used to identify the request number for the current test. This is used to ensure that the mock server response is deterministic for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
source: { header: 'X-Test-Request-Number' },
status: '500',
code: 'MOCK_SERVER_ERROR',
title: 'Mock Server Error during Request',
detail: e.message,
},
],
})
);
}

if (req.method === 'POST' || niceUrl === '__record') {
const payload = await req.json();
const { url, headers, method, status, statusText, body, response } = payload;
const cacheKey = generateFilepath({
projectRoot,
testId,
url,
method,
body: body ? JSON.stringify(body) : null,
testRequestNumber,
});
// allow Content-Type to be overridden
headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
// We always compress and chunk the response
headers['Content-Encoding'] = 'br';
// we don't cache since tests will often reuse similar urls for different payload
headers['Cache-Control'] = 'no-store';

const cacheDir = generateFileDir({
projectRoot,
testId,
url,
method,
testRequestNumber,
});

fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(
`${cacheKey}.meta.json`,
JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2)
);
fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response)));
context.status(204);
return context.body(null);
} else {
const body = await req.text();
const cacheKey = generateFilepath({
projectRoot,
testId,
url: niceUrl,
method: req.method,
body,
testRequestNumber,
});
return replayRequest(context, cacheKey);
}
};

return TestHandler;
Expand Down
2 changes: 1 addition & 1 deletion packages/request-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"peerDependencies": {
"@ember/string": "^3.1.1 || ^4.0.0",
"@warp-drive/core-types": "workspace:0.0.0-alpha.93",
"ember-inflector": "4.0.2"
"ember-inflector": "^4.0.2 || ^5.0.0"
},
"peerDependenciesMeta": {
"ember-inflector": {
Expand Down
Loading

0 comments on commit 1be0954

Please sign in to comment.