@stjoseph/prumo 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prumo-templates/core/testing.md +14 -4
- package/README.md +12 -1
- package/dist/names.js +13 -1
- package/dist/pnpm-extended.js +159 -0
- package/package.json +4 -1
- package/templates/mobile/test/features/auth/authenticated-layout.spec.tsx +3 -2
- package/templates/mobile/test/features/auth/email-flows.spec.tsx +3 -2
- package/templates/mobile/test/features/auth/sign-in-form.spec.tsx +3 -2
- package/templates/mobile/test/features/auth/sign-out-button.spec.tsx +3 -2
- package/templates/mobile/test/features/auth/social-buttons.spec.tsx +3 -2
- package/templates/mobile/test/features/profile/profile-form.spec.tsx +3 -4
- package/templates/mobile/test/support/query-client.ts +14 -0
- package/templates/web/test/features/oauth/consent-form.spec.tsx +3 -2
- package/templates/web/test/features/profile/profile-form.spec.tsx +3 -2
- package/templates/web/test/support/query-client.ts +14 -0
- package/templates/web/test/support/render-app.tsx +2 -2
- package/templates/web/vite.config.ts +10 -3
|
@@ -15,7 +15,8 @@ Do not mock `EntityManager` or any concrete class from a library.
|
|
|
15
15
|
|
|
16
16
|
**In a client, replace the network through the injected transport**: pass a fake `fetch` to `createClient`.
|
|
17
17
|
Do not add a request-mocking library. Test a component that shows an error by rendering it and asserting
|
|
18
|
-
**where** the error appears.
|
|
18
|
+
**where** the error appears. Build every test's `QueryClient` with `createTestQueryClient()` from
|
|
19
|
+
`@test/support/query-client`, never with `new QueryClient()`: the defaults a test needs live there, once.
|
|
19
20
|
|
|
20
21
|
Measure coverage and report it. Do not block a merge on a coverage threshold.
|
|
21
22
|
|
|
@@ -59,9 +60,18 @@ written in a mobile test is a mistake an assistant will make.
|
|
|
59
60
|
is **asynchronous** and its matchers need no setup. `expo-secure-store` needs the native Keychain or Keystore,
|
|
60
61
|
which Jest does not have, so a test builds the auth client with an in-memory token store, which is why a form
|
|
61
62
|
asks only for the auth methods it calls. And `jest-expo` leaves the app manifest empty, so `expo-linking` cannot
|
|
62
|
-
know the app's scheme: a test that reaches `Linking.createURL` mocks it with the scheme it expects.
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
know the app's scheme: a test that reaches `Linking.createURL` mocks it with the scheme it expects. And a query
|
|
64
|
+
or mutation left without an observer once a test unmounts starts TanStack Query's five-minute garbage-collection
|
|
65
|
+
timer, which Jest's worker waits on until it is force-exited. `createTestQueryClient()` sets `gcTime: Infinity`
|
|
66
|
+
for both, so no timer starts, and `retry: false` for queries, so a failure shows at once: the two defaults TanStack
|
|
67
|
+
Query's testing guide gives. Vitest does not wait on the timer, and `web` uses the same helper so that one rule
|
|
68
|
+
holds in both.
|
|
69
|
+
|
|
70
|
+
**`web` turns route code splitting off under Vitest**, in `vite.config.ts`. Split, a route's component is a module
|
|
71
|
+
Vite transforms the first time a test renders it, inside the one second a `find*` query waits, and on a loaded CI
|
|
72
|
+
runner the first page of a test file missed it. The wait is not the problem and is not raised: with the transform
|
|
73
|
+
gone the same load passes, and past that load a longer wait fails the same tests anyway. **What it costs:** the
|
|
74
|
+
tests never load a route's lazy chunk; the build and its preview do.
|
|
65
75
|
|
|
66
76
|
A coverage threshold turns a proxy into a target. Whoever is below writes tests to raise the number rather
|
|
67
77
|
than to check anything; whoever is above stops thinking. With no database mocks a test costs more to write,
|
package/README.md
CHANGED
|
@@ -60,7 +60,9 @@ committed, so the first commit is yours and the diff shows everything Prumo wrot
|
|
|
60
60
|
writes into the current directory instead, which must be empty, and names the project after it. If
|
|
61
61
|
anything fails or is interrupted, Prumo removes what it wrote, so the same command can run again.
|
|
62
62
|
A project cannot be named `api`, `web`, `mobile` or `site`: those are the names its apps take inside
|
|
63
|
-
a workspace.
|
|
63
|
+
a workspace. Nor can it take the name of a package that pnpm's compatibility database extends, such as `ws`:
|
|
64
|
+
pnpm applies the extension to the project's own `package.json` and installs what it adds. Prumo names
|
|
65
|
+
the packages and suggests another name.
|
|
64
66
|
|
|
65
67
|
Every question also has a flag, which is how CI runs it without a terminal. Outside an interactive
|
|
66
68
|
terminal every question that applies needs its flag, the layout of a single type included: a missing
|
|
@@ -138,6 +140,7 @@ Once written, those documents belong to the team. Prumo does not come back and r
|
|
|
138
140
|
| [`docs/structure.md`](https://github.com/stjosephworks/prumo/blob/main/docs/structure.md) | The `.prumo/` layout, and the rule deciding which areas a project receives |
|
|
139
141
|
| [`docs/document-template.md`](https://github.com/stjosephworks/prumo/blob/main/docs/document-template.md) | The shape every convention document fills |
|
|
140
142
|
| [`docs/maintaining-templates.md`](https://github.com/stjosephworks/prumo/blob/main/docs/maintaining-templates.md) | How a template is changed, and how the change is proved |
|
|
143
|
+
| [`docs/upgrading.md`](https://github.com/stjosephworks/prumo/blob/main/docs/upgrading.md) | What each release changed in the files it generates, and how an existing project carries it over |
|
|
141
144
|
|
|
142
145
|
## Releasing
|
|
143
146
|
|
|
@@ -145,6 +148,14 @@ Once written, those documents belong to the team. Prumo does not come back and r
|
|
|
145
148
|
registry does not have yet, the `publish` job sends it to npm through npm's trusted publishing, with provenance and
|
|
146
149
|
without a token in the repository. Any other push to `main` publishes nothing.
|
|
147
150
|
|
|
151
|
+
## Upgrading a project
|
|
152
|
+
|
|
153
|
+
Prumo does not update a project it generated, and there is no `prumo upgrade`. For each release that changed
|
|
154
|
+
the generated files, [`docs/upgrading.md`](https://github.com/stjosephworks/prumo/blob/main/docs/upgrading.md)
|
|
155
|
+
lists what to copy or change and why, and gives one method: generate a reference project with the same answers
|
|
156
|
+
at the old and the new version, and diff the files listed. A file your team has edited is merged by hand, never
|
|
157
|
+
overwritten.
|
|
158
|
+
|
|
148
159
|
## Status
|
|
149
160
|
|
|
150
161
|
Prumo is in beta: every template runs and every generated project is verified in CI, and what a generated project looks like can still change before 1.0.
|
package/dist/names.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, resolve } from 'node:path';
|
|
3
|
+
import { EXTENDED_BY_PNPM } from "./pnpm-extended.js";
|
|
3
4
|
const PROJECT_NAME = /^[a-z][a-z0-9-]*$/;
|
|
4
5
|
// npm refuses a longer package name.
|
|
5
6
|
const MAX_LENGTH = 214;
|
|
@@ -15,7 +16,18 @@ export function validateProjectName(name) {
|
|
|
15
16
|
if (RESERVED.includes(name)) {
|
|
16
17
|
return 'That is the name an app takes inside a workspace; choose another.';
|
|
17
18
|
}
|
|
18
|
-
return
|
|
19
|
+
return extendedByPnpm(name);
|
|
20
|
+
}
|
|
21
|
+
function extendedByPnpm(name) {
|
|
22
|
+
const added = Object.hasOwn(EXTENDED_BY_PNPM, name) ? EXTENDED_BY_PNPM[name] : undefined;
|
|
23
|
+
if (added === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const shown = added.length > 3
|
|
27
|
+
? `${added.slice(0, 3).join(', ')} and ${added.length - 3} more`
|
|
28
|
+
: added.join(', ');
|
|
29
|
+
return (`pnpm extends the npm package "${name}" with ${shown}, and would install them into a project of that name. ` +
|
|
30
|
+
`Choose another, such as "${name}-app".`);
|
|
19
31
|
}
|
|
20
32
|
// Why a directory cannot receive a project, or undefined when it can: it does not exist yet, or it is empty.
|
|
21
33
|
export function targetProblem(target) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// pnpm applies a compatibility database, @yarnpkg/extensions, to every manifest it reads: the registry's packages and
|
|
2
|
+
// the project's own package.json alike. An entry matches by name and version range, and the template version 0.0.1
|
|
3
|
+
// falls inside ranges like `ws@<7.2.1`. A project carrying one of these names therefore gains the dependencies the
|
|
4
|
+
// entry adds, installed into its root, and a package among them with a build script stops `pnpm install` with
|
|
5
|
+
// ERR_PNPM_IGNORED_BUILDS. Each name maps to the packages it would add.
|
|
6
|
+
//
|
|
7
|
+
// Derived from @yarnpkg/extensions, pinned in devDependencies, by test/names.spec.ts, which fails when this list and
|
|
8
|
+
// the database part ways. Only names a project can take are kept, and only entries that add a package: one that sets
|
|
9
|
+
// nothing but peerDependenciesMeta changes no install.
|
|
10
|
+
export const EXTENDED_BY_PNPM = {
|
|
11
|
+
'apollo-upload-client': ['graphql'],
|
|
12
|
+
'auto-relay': ['reflect-metadata'],
|
|
13
|
+
'babel-plugin-graphql-tag': ['graphql'],
|
|
14
|
+
'babel-plugin-remove-graphql-queries': ['@babel/runtime', '@babel/types', 'gatsby-core-utils'],
|
|
15
|
+
'babel-plugin-transform-typescript-metadata': ['@babel/core', '@babel/traverse'],
|
|
16
|
+
'babel-preset-gatsby-package': ['@babel/runtime'],
|
|
17
|
+
'bin-links': ['mkdirp-infer-owner'],
|
|
18
|
+
'cacheable-lookup': ['@types/keyv'],
|
|
19
|
+
'clipanion-v3-codemod': ['jscodeshift'],
|
|
20
|
+
'connect-mongo': ['express-session'],
|
|
21
|
+
consolidate: [
|
|
22
|
+
'arc-templates',
|
|
23
|
+
'atpl',
|
|
24
|
+
'babel-core',
|
|
25
|
+
'bracket-template',
|
|
26
|
+
'coffee-script',
|
|
27
|
+
'dot',
|
|
28
|
+
'dust',
|
|
29
|
+
'dustjs-helpers',
|
|
30
|
+
'dustjs-linkedin',
|
|
31
|
+
'eco',
|
|
32
|
+
'ect',
|
|
33
|
+
'ejs',
|
|
34
|
+
'haml-coffee',
|
|
35
|
+
'hamlet',
|
|
36
|
+
'hamljs',
|
|
37
|
+
'handlebars',
|
|
38
|
+
'hogan.js',
|
|
39
|
+
'htmling',
|
|
40
|
+
'jade',
|
|
41
|
+
'jazz',
|
|
42
|
+
'jqtpl',
|
|
43
|
+
'just',
|
|
44
|
+
'liquid-node',
|
|
45
|
+
'liquor',
|
|
46
|
+
'lodash',
|
|
47
|
+
'marko',
|
|
48
|
+
'mote',
|
|
49
|
+
'mustache',
|
|
50
|
+
'nunjucks',
|
|
51
|
+
'plates',
|
|
52
|
+
'pug',
|
|
53
|
+
'qejs',
|
|
54
|
+
'ractive',
|
|
55
|
+
'razor-tmpl',
|
|
56
|
+
'react',
|
|
57
|
+
'react-dom',
|
|
58
|
+
'slm',
|
|
59
|
+
'squirrelly',
|
|
60
|
+
'swig',
|
|
61
|
+
'swig-templates',
|
|
62
|
+
'teacup',
|
|
63
|
+
'templayed',
|
|
64
|
+
'then-jade',
|
|
65
|
+
'then-pug',
|
|
66
|
+
'tinyliquid',
|
|
67
|
+
'toffee',
|
|
68
|
+
'twig',
|
|
69
|
+
'twing',
|
|
70
|
+
'underscore',
|
|
71
|
+
'vash',
|
|
72
|
+
'velocityjs',
|
|
73
|
+
'walrus',
|
|
74
|
+
'whiskers',
|
|
75
|
+
],
|
|
76
|
+
'cordova-ios': ['underscore'],
|
|
77
|
+
'cordova-lib': ['underscore'],
|
|
78
|
+
'create-gatsby': ['@babel/runtime'],
|
|
79
|
+
'elm-webpack-loader': ['temp'],
|
|
80
|
+
'eslint-import-resolver-vite': ['debug', 'resolve'],
|
|
81
|
+
fdir: ['picomatch'],
|
|
82
|
+
'fork-ts-checker-webpack-plugin': ['eslint', 'typescript', 'vue-template-compiler', 'webpack'],
|
|
83
|
+
'gatsby-admin': ['@babel/runtime'],
|
|
84
|
+
'gatsby-cli': ['@babel/runtime'],
|
|
85
|
+
'gatsby-core-utils': ['@babel/runtime', 'got'],
|
|
86
|
+
'gatsby-design-tokens': ['@babel/runtime'],
|
|
87
|
+
'gatsby-legacy-polyfills': ['@babel/runtime'],
|
|
88
|
+
'gatsby-plugin-benchmark-reporting': ['@babel/runtime'],
|
|
89
|
+
'gatsby-plugin-favicon': ['webpack'],
|
|
90
|
+
'gatsby-plugin-gatsby-cloud': ['gatsby-core-utils', 'webpack'],
|
|
91
|
+
'gatsby-plugin-graphql-config': ['@babel/runtime'],
|
|
92
|
+
'gatsby-plugin-i18n': ['ramda'],
|
|
93
|
+
'gatsby-plugin-image': ['@babel/runtime'],
|
|
94
|
+
'gatsby-plugin-mdx': ['@babel/runtime', 'mkdirp'],
|
|
95
|
+
'gatsby-plugin-netlify-cms': ['@babel/runtime'],
|
|
96
|
+
'gatsby-plugin-no-sourcemaps': ['@babel/runtime'],
|
|
97
|
+
'gatsby-plugin-page-creator': ['@babel/runtime', 'fs-extra'],
|
|
98
|
+
'gatsby-plugin-preact': ['@babel/runtime'],
|
|
99
|
+
'gatsby-plugin-preload-fonts': ['@babel/runtime'],
|
|
100
|
+
'gatsby-plugin-schema-snapshot': ['@babel/runtime'],
|
|
101
|
+
'gatsby-plugin-sharp': ['debug'],
|
|
102
|
+
'gatsby-plugin-styletron': ['@babel/runtime'],
|
|
103
|
+
'gatsby-plugin-subfont': ['@babel/runtime'],
|
|
104
|
+
'gatsby-plugin-utils': ['@babel/runtime', 'fastq', 'graphql'],
|
|
105
|
+
'gatsby-react-router-scroll': ['prop-types'],
|
|
106
|
+
'gatsby-recipes': ['@babel/runtime'],
|
|
107
|
+
'gatsby-remark-prismjs': ['lodash'],
|
|
108
|
+
'gatsby-source-apiserver': ['babel-polyfill'],
|
|
109
|
+
'gatsby-source-shopify': ['@babel/runtime'],
|
|
110
|
+
'gatsby-source-wikipedia': ['@babel/runtime'],
|
|
111
|
+
'gatsby-transformer-screenshot': ['@babel/runtime'],
|
|
112
|
+
'gatsby-worker': ['@babel/runtime'],
|
|
113
|
+
'git-node-fs': ['js-git'],
|
|
114
|
+
got: ['@types/keyv', '@types/responselike'],
|
|
115
|
+
'http-link-dataloader': ['graphql'],
|
|
116
|
+
'ink-select-input': ['react'],
|
|
117
|
+
'jest-vue-preprocessor': ['@babel/core', '@babel/template', 'pug'],
|
|
118
|
+
'jss-plugin-rule-value-function': ['tiny-warning'],
|
|
119
|
+
mqtt: ['duplexify'],
|
|
120
|
+
'query-ast': ['lodash'],
|
|
121
|
+
'rc-animate': ['react', 'react-dom'],
|
|
122
|
+
'react-bootstrap-table2-paginator': ['classnames'],
|
|
123
|
+
'react-color': ['react'],
|
|
124
|
+
'react-dev-utils': ['typescript', 'webpack'],
|
|
125
|
+
'react-draggable': ['react', 'react-dom'],
|
|
126
|
+
'react-github-btn': ['react'],
|
|
127
|
+
'react-instantsearch-core': ['algoliasearch'],
|
|
128
|
+
'react-instantsearch-dom': ['react-fast-compare'],
|
|
129
|
+
'react-live': ['react', 'react-dom'],
|
|
130
|
+
'react-portal': ['react-dom'],
|
|
131
|
+
'react-rnd': ['react', 'react-dom'],
|
|
132
|
+
'react-scripts': ['eslint', 'react'],
|
|
133
|
+
reactcss: ['react'],
|
|
134
|
+
rebass: ['react'],
|
|
135
|
+
'redux-persist': ['react'],
|
|
136
|
+
'redux-thunk': ['redux'],
|
|
137
|
+
'rollup-plugin-polyfill-node': ['rollup'],
|
|
138
|
+
'scss-parser': ['lodash'],
|
|
139
|
+
skypack: ['tar'],
|
|
140
|
+
snowpack: ['magic-string'],
|
|
141
|
+
terser: ['acorn'],
|
|
142
|
+
testcafe: ['@babel/plugin-transform-for-of', '@babel/runtime'],
|
|
143
|
+
'testcafe-legacy-api': ['read-file-relative', 'testcafe-hammerhead'],
|
|
144
|
+
'typescript-language-server': ['vscode-jsonrpc', 'vscode-languageserver-protocol'],
|
|
145
|
+
unified: ['@types/unist'],
|
|
146
|
+
'unplugin-vue2-script-setup': ['@vue/composition-api', '@vue/runtime-dom'],
|
|
147
|
+
'vite-plugin-vuetify': ['vue'],
|
|
148
|
+
'volar-service-typescript': ['typescript'],
|
|
149
|
+
'volar-service-typescript-twoslash-queries': ['typescript'],
|
|
150
|
+
'vue-cli-plugin-vuetify': ['null-loader', 'semver'],
|
|
151
|
+
'vue-i18n': ['vue'],
|
|
152
|
+
'vue-loader': ['@vue/compiler-sfc', 'webpack'],
|
|
153
|
+
'vue-router': ['vue'],
|
|
154
|
+
'vue-template-babel-compiler': ['vue-template-compiler'],
|
|
155
|
+
'webpack-plugin-vuetify': ['vue'],
|
|
156
|
+
'winston-transport': ['logform'],
|
|
157
|
+
ws: ['bufferutil', 'utf-8-validate'],
|
|
158
|
+
xo: ['webpack'],
|
|
159
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stjoseph/prumo",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Scaffolds a TypeScript project and ships it the conventions it follows, in a .prumo/ folder any AI assistant reads.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"scaffolding",
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@biomejs/biome": "2.5.13",
|
|
50
50
|
"@types/node": "24.13.4",
|
|
51
|
+
"@types/semver": "7.8.0",
|
|
52
|
+
"@yarnpkg/extensions": "2.0.7",
|
|
53
|
+
"semver": "7.8.5",
|
|
51
54
|
"typescript": "6.0.3",
|
|
52
55
|
"vitest": "4.1.11"
|
|
53
56
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
3
3
|
import { memoryTokenStore } from '@test/support/memory-token-store'
|
|
4
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
4
5
|
import { renderRouter, screen } from 'expo-router/testing-library'
|
|
5
6
|
import type { ReactNode } from 'react'
|
|
6
7
|
import { Text } from 'react-native'
|
|
@@ -23,7 +24,7 @@ async function renderAt(getSession: () => Response | Promise<Response>) {
|
|
|
23
24
|
tokens: memoryTokenStore(TOKENS),
|
|
24
25
|
})
|
|
25
26
|
const clients = { auth, api: createClient({ baseUrl: API_URL, fetch: auth.fetch }) }
|
|
26
|
-
const queryClient =
|
|
27
|
+
const queryClient = createTestQueryClient()
|
|
27
28
|
|
|
28
29
|
function Providers({ children }: { children: ReactNode }) {
|
|
29
30
|
return (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
3
3
|
import { memoryTokenStore } from '@test/support/memory-token-store'
|
|
4
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
4
5
|
import { userEvent } from '@testing-library/react-native'
|
|
5
6
|
import { renderRouter, screen } from 'expo-router/testing-library'
|
|
6
7
|
import type { ReactNode } from 'react'
|
|
@@ -18,7 +19,7 @@ async function renderAt(initialUrl: string, routes: Parameters<typeof fakeTransp
|
|
|
18
19
|
const transport = fakeTransport(routes)
|
|
19
20
|
const auth = createAuthClient({ baseUrl: API_URL, fetch: transport, tokens: memoryTokenStore() })
|
|
20
21
|
const clients = { auth, api: createClient({ baseUrl: API_URL, fetch: auth.fetch }) }
|
|
21
|
-
const queryClient =
|
|
22
|
+
const queryClient = createTestQueryClient()
|
|
22
23
|
|
|
23
24
|
function Providers({ children }: { children: ReactNode }) {
|
|
24
25
|
return (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
3
3
|
import { memoryTokenStore } from '@test/support/memory-token-store'
|
|
4
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
4
5
|
import { render, screen, userEvent } from '@testing-library/react-native'
|
|
5
6
|
import { createAuthClient } from '@/api-contract'
|
|
6
7
|
import { SignInForm } from '@/features/auth/sign-in-form'
|
|
@@ -23,7 +24,7 @@ describe('SignInForm', () => {
|
|
|
23
24
|
const user = userEvent.setup()
|
|
24
25
|
|
|
25
26
|
await render(
|
|
26
|
-
<QueryClientProvider client={
|
|
27
|
+
<QueryClientProvider client={createTestQueryClient()}>
|
|
27
28
|
<SignInForm auth={auth} onSignedIn={onSignedIn} />
|
|
28
29
|
</QueryClientProvider>,
|
|
29
30
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport } from '@test/support/fake-transport'
|
|
3
3
|
import { memoryTokenStore } from '@test/support/memory-token-store'
|
|
4
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
4
5
|
import { render, screen, userEvent, waitFor } from '@testing-library/react-native'
|
|
5
6
|
import { createAuthClient } from '@/api-contract'
|
|
6
7
|
import { SignOutButton } from '@/features/auth/sign-out-button'
|
|
@@ -10,7 +11,7 @@ import { clearUserData } from '@/features/storage/storage'
|
|
|
10
11
|
jest.mock('@/features/storage/storage', () => ({ clearUserData: jest.fn() }))
|
|
11
12
|
|
|
12
13
|
async function signOutWith(signOut: () => Response | Promise<Response>) {
|
|
13
|
-
const queryClient =
|
|
14
|
+
const queryClient = createTestQueryClient()
|
|
14
15
|
const onSignedOut = jest.fn()
|
|
15
16
|
const auth = createAuthClient({
|
|
16
17
|
baseUrl: API_URL,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
3
3
|
import { memoryTokenStore } from '@test/support/memory-token-store'
|
|
4
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
4
5
|
import { render, screen, userEvent, waitFor } from '@testing-library/react-native'
|
|
5
6
|
import * as WebBrowser from 'expo-web-browser'
|
|
6
7
|
import { createAuthClient } from '@/api-contract'
|
|
@@ -28,7 +29,7 @@ async function renderButtons(exchange: () => Response) {
|
|
|
28
29
|
})
|
|
29
30
|
|
|
30
31
|
await render(
|
|
31
|
-
<QueryClientProvider client={
|
|
32
|
+
<QueryClientProvider client={createTestQueryClient()}>
|
|
32
33
|
<SocialButtons auth={auth} onSignedIn={onSignedIn} />
|
|
33
34
|
</QueryClientProvider>,
|
|
34
35
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
3
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
3
4
|
import { render, screen, userEvent, within } from '@testing-library/react-native'
|
|
4
5
|
import { createClient, type Profile } from '@/api-contract'
|
|
5
6
|
import { ProfileForm } from '@/features/profile/profile-form'
|
|
@@ -36,9 +37,7 @@ describe('ProfileForm', () => {
|
|
|
36
37
|
}),
|
|
37
38
|
})
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
// it out. Infinity schedules none, as TanStack Query's testing guide advises.
|
|
41
|
-
const queryClient = new QueryClient({ defaultOptions: { mutations: { gcTime: Infinity } } })
|
|
40
|
+
const queryClient = createTestQueryClient()
|
|
42
41
|
|
|
43
42
|
await render(
|
|
44
43
|
<QueryClientProvider client={queryClient}>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
// Every test builds its QueryClient here, with the defaults TanStack Query's testing guide gives. A failed query
|
|
4
|
+
// fails at once instead of retrying three times with backoff. And gcTime Infinity schedules no garbage-collection
|
|
5
|
+
// timer: once a test unmounts, a query or mutation left without an observer would otherwise start a real five-minute
|
|
6
|
+
// setTimeout, and Jest's worker waits on it until it is force-exited.
|
|
7
|
+
export function createTestQueryClient(): QueryClient {
|
|
8
|
+
return new QueryClient({
|
|
9
|
+
defaultOptions: {
|
|
10
|
+
queries: { retry: false, gcTime: Infinity },
|
|
11
|
+
mutations: { gcTime: Infinity },
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
3
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
3
4
|
import { renderApp } from '@test/support/render-app'
|
|
4
5
|
import { render, screen } from '@testing-library/react'
|
|
5
6
|
import userEvent from '@testing-library/user-event'
|
|
@@ -26,7 +27,7 @@ function renderForm(routes: Parameters<typeof fakeTransport>[0]) {
|
|
|
26
27
|
const auth = createAuthClient({ baseUrl: API_URL, fetch: fakeTransport(routes) })
|
|
27
28
|
|
|
28
29
|
render(
|
|
29
|
-
<QueryClientProvider client={
|
|
30
|
+
<QueryClientProvider client={createTestQueryClient()}>
|
|
30
31
|
<ConsentForm auth={auth} requestId={REQUEST} leave={leave} />
|
|
31
32
|
</QueryClientProvider>,
|
|
32
33
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
3
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
3
4
|
import { render, screen } from '@testing-library/react'
|
|
4
5
|
import userEvent from '@testing-library/user-event'
|
|
5
6
|
import { describe, expect, it } from 'vitest'
|
|
@@ -40,7 +41,7 @@ describe('ProfileForm', () => {
|
|
|
40
41
|
})
|
|
41
42
|
|
|
42
43
|
render(
|
|
43
|
-
<QueryClientProvider client={
|
|
44
|
+
<QueryClientProvider client={createTestQueryClient()}>
|
|
44
45
|
<ProfileForm api={api} profile={profile} />
|
|
45
46
|
</QueryClientProvider>,
|
|
46
47
|
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
// Every test builds its QueryClient here, with the defaults TanStack Query's testing guide gives. A failed query
|
|
4
|
+
// fails at once instead of retrying three times with backoff. And gcTime Infinity schedules no garbage-collection
|
|
5
|
+
// timer: once a test unmounts, a query or mutation left without an observer would otherwise start a real five-minute
|
|
6
|
+
// setTimeout, and Jest's worker waits on it until it is force-exited.
|
|
7
|
+
export function createTestQueryClient(): QueryClient {
|
|
8
|
+
return new QueryClient({
|
|
9
|
+
defaultOptions: {
|
|
10
|
+
queries: { retry: false, gcTime: Infinity },
|
|
11
|
+
mutations: { gcTime: Infinity },
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { QueryClient } from '@tanstack/react-query'
|
|
2
1
|
import { createMemoryHistory } from '@tanstack/react-router'
|
|
3
2
|
import { API_URL } from '@test/support/fake-transport'
|
|
3
|
+
import { createTestQueryClient } from '@test/support/query-client'
|
|
4
4
|
import { render } from '@testing-library/react'
|
|
5
5
|
import { createAuthClient, createClient, type Transport } from '@/api-contract'
|
|
6
6
|
import { App } from '@/app'
|
|
@@ -8,7 +8,7 @@ import { App } from '@/app'
|
|
|
8
8
|
export function renderApp(path: string, transport: Transport) {
|
|
9
9
|
const auth = createAuthClient({ baseUrl: API_URL, fetch: transport })
|
|
10
10
|
const context = {
|
|
11
|
-
queryClient:
|
|
11
|
+
queryClient: createTestQueryClient(),
|
|
12
12
|
api: createClient({ baseUrl: API_URL, fetch: auth.fetch }),
|
|
13
13
|
auth,
|
|
14
14
|
}
|
|
@@ -3,8 +3,15 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite'
|
|
|
3
3
|
import react from '@vitejs/plugin-react'
|
|
4
4
|
import { defineConfig } from 'vitest/config'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// Under Vitest (mode `test`) every route is imported with the route tree. Split, each route's component is a
|
|
7
|
+
// virtual module Vite transforms the first time a test renders it, inside the one second Testing Library waits for
|
|
8
|
+
// the page, and on a loaded machine that transform alone outlasted the wait.
|
|
9
|
+
export default defineConfig(({ mode }) => ({
|
|
10
|
+
plugins: [
|
|
11
|
+
tanstackRouter({ target: 'react', autoCodeSplitting: mode !== 'test' }),
|
|
12
|
+
react(),
|
|
13
|
+
tailwindcss(),
|
|
14
|
+
],
|
|
8
15
|
resolve: {
|
|
9
16
|
alias: { '@': `${import.meta.dirname}/src`, '@test': `${import.meta.dirname}/test` },
|
|
10
17
|
},
|
|
@@ -15,4 +22,4 @@ export default defineConfig({
|
|
|
15
22
|
setupFiles: ['./test/support/setup.ts'],
|
|
16
23
|
coverage: { provider: 'v8', include: ['src/**'] },
|
|
17
24
|
},
|
|
18
|
-
})
|
|
25
|
+
}))
|