From 7aa92bba427d6870081f00f6911636ac525a784a Mon Sep 17 00:00:00 2001 From: Tim Leslie Date: Thu, 24 Jun 2021 12:47:51 +1000 Subject: [PATCH] @keystone-next/testing example (#5887) --- .github/workflows/tests.yml | 37 +- examples/README.md | 1 + examples/testing/CHANGELOG.md | 1 + examples/testing/README.md | 119 +++++ examples/testing/babel.config.js | 32 ++ examples/testing/example.test.ts | 193 ++++++++ examples/testing/keystone.ts | 45 ++ examples/testing/package.json | 31 ++ examples/testing/schema.graphql | 507 +++++++++++++++++++++ examples/testing/schema.prisma | 29 ++ examples/testing/schema.ts | 41 ++ tests/examples-smoke-tests/testing.test.ts | 16 + yarn.lock | 184 +++++++- 13 files changed, 1227 insertions(+), 9 deletions(-) create mode 100644 examples/testing/CHANGELOG.md create mode 100644 examples/testing/README.md create mode 100644 examples/testing/babel.config.js create mode 100644 examples/testing/example.test.ts create mode 100644 examples/testing/keystone.ts create mode 100644 examples/testing/package.json create mode 100644 examples/testing/schema.graphql create mode 100644 examples/testing/schema.prisma create mode 100644 examples/testing/schema.ts create mode 100644 tests/examples-smoke-tests/testing.test.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 56e4c89e02a..9f796a13fab 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -198,12 +198,46 @@ jobs: - name: Install Dependencies run: yarn - name: Unit tests - run: yarn jest --ci --maxWorkers=1 --testPathIgnorePatterns=api-tests --testPathIgnorePatterns=examples-smoke-tests + run: yarn jest --ci --maxWorkers=1 --testPathIgnorePatterns=api-tests --testPathIgnorePatterns=examples-smoke-tests --testPathIgnorePatterns=examples/testing env: CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }} CLOUDINARY_KEY: ${{ secrets.CLOUDINARY_KEY }} CLOUDINARY_SECRET: ${{ secrets.CLOUDINARY_SECRET }} + example-testing: + name: Testing example project + needs: should_run_tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + + - name: Setup Node.js 14.x + uses: actions/setup-node@main + with: + node-version: 14.x + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + node_modules + key: ${{ runner.os }}-yarn-v4-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-v4- + + - name: Install Dependencies + run: yarn + - name: Unit tests + run: cd examples/testing; yarn test + examples_smoke_tests: name: Smoke Tests For Examples runs-on: ubuntu-latest @@ -222,6 +256,7 @@ jobs: 'json.test.ts', 'roles.test.ts', 'task-manager.test.ts', + 'testing.test.ts', 'with-auth.test.ts', ] fail-fast: false diff --git a/examples/README.md b/examples/README.md index 080a72d73b8..6e06077e293 100644 --- a/examples/README.md +++ b/examples/README.md @@ -22,6 +22,7 @@ Each project below demonstrates a Keystone feature you can learn about and exper - [`defaultValue`](./default-values): Adds default values to the Blog base. - [`extendGraphqlSchema`](./extend-graphql-schema): Extends the GraphQL API of the Task Manager base. - [`virtual field`](./virtual-field): Adds virtual fields to the Blog base. +- [Testing](./testing): Adds tests with `@keystone-next/testing` to the `withAuth()` example. ## Running examples diff --git a/examples/testing/CHANGELOG.md b/examples/testing/CHANGELOG.md new file mode 100644 index 00000000000..e0f022f74d2 --- /dev/null +++ b/examples/testing/CHANGELOG.md @@ -0,0 +1 @@ +# @keystone-next/example-testing diff --git a/examples/testing/README.md b/examples/testing/README.md new file mode 100644 index 00000000000..f5cbbc9c3ca --- /dev/null +++ b/examples/testing/README.md @@ -0,0 +1,119 @@ +## Feature Example - Testing + +This project demonstrates how to write tests against the GraphQL API to your Keystone system. +It builds on the [`withAuth()`](../with-auth) example project. + +## Instructions + +To run this project, clone the Keystone repository locally then navigate to this directory and run: + +```shell +yarn dev +``` + +This will start the Admin UI at [localhost:3000](http://localhost:3000). +You can use the Admin UI to create items in your database. + +You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations. + +## Features + +Keystone provides a testing library in the package [`@keystone-next/testing`](https://next.keystonejs.com/guides/testing) which helps you write tests using [Jest](https://jestjs.io/). +This example project uses this library to add tests to the [`withAuth()`](../with-auth) example project. The tests can be found in [example.test.ts](./example.test.ts) + +### Test runner + +The function `setupTestRunner` takes the project's `KeystoneConfig` object and creates a `runner` function. This test runner is then used to wrap a test function. + +```typescript +import { setupTestRunner } from '@keystone-next/testing'; +import config from './keystone'; + +const runner = setupTestRunner({ config }); + +describe('Example tests using test runner', () => { + test( + 'Create a Person using the list items API', + runner(async ({ context }) => { + ... + }) + ); +}); +``` + +For each test, the runner will connect to the database and drop all the data so that the test can run in a known state, and also handle disconnecting from the database after the test. + +#### `context` + +The test runner provides the test function with a [`KeystoneContext`](https://next.keystonejs.com/apis/context) argument called `context`. This is the main API for interacting with the Keystone system. It can be used to read and write data to the system and verify that the system is behaving as expected. + +```typescript +test( + 'Create a Person using the list items API', + runner(async ({ context }) => { + const person = await context.lists.Person.createOne({ + data: { name: 'Alice', email: 'alice@example.com', password: 'super-secret' }, + query: 'id name email password { isSet }', + }); + expect(person.name).toEqual('Alice'); + expect(person.email).toEqual('alice@example.com'); + expect(person.password.isSet).toEqual(true); + }) +); +``` + +#### `graphQLRequest` + +The test runner also provides the function with an argument `graphQLRequest`, which is a [`supertest`](https://github.com/visionmedia/supertest) request configured to accept arguments `{ query, variable, operation }` and send them to your GraphQL API. You can use the `supertest` API to set additional request headers and to check that the response returned is what you expected. + +```typescript +test( + 'Create a Person using a hand-crafted GraphQL query sent over HTTP', + runner(async ({ graphQLRequest }) => { + const { body } = await graphQLRequest({ + query: `mutation { + createPerson(data: { name: "Alice", email: "alice@example.com", password: "super-secret" }) { + id name email password { isSet } + } + }`, + }).expect(200); + const person = body.data.createPerson; + expect(person.name).toEqual('Alice'); + expect(person.email).toEqual('alice@example.com'); + expect(person.password.isSet).toEqual(true); + }) +); +``` + +### Test environment + +The function `setupTestEnv` is used to set up a test environment which can be used across multiple tests. + +```typescript +import { KeystoneContext } from '@keystone-next/types'; +import { setupTestEnv, TestEnv } from '@keystone-next/testing'; +import config from './keystone'; + +describe('Example tests using test environment', () => { + let testEnv: TestEnv, context: KeystoneContext; + beforeAll(async () => { + testEnv = await setupTestEnv({ config }); + context = testEnv.testArgs.context; + + await testEnv.connect(); + + // Perform any setup such as data seeding here. + }); + afterAll(async () => { + await testEnv.disconnect(); + }); + + test('Check that the persons password is set', async () => { + ... + }); +}); +``` + +`setupTestEnv` will connect to the database and drop all the data so that the test can run in a known state, returning a value `testEnv` which contains `{ connect, disconnect, testArgs }`. +The value `testArgs` contains the same values that are passed into test functions by the test runner. +The `connect` and `disconnect` functions are used to connect to the database before the tests run, then disconnect once all tests have completed. diff --git a/examples/testing/babel.config.js b/examples/testing/babel.config.js new file mode 100644 index 00000000000..a8114fee85d --- /dev/null +++ b/examples/testing/babel.config.js @@ -0,0 +1,32 @@ +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 10, + browsers: [ + 'last 2 chrome versions', + 'last 2 firefox versions', + 'last 2 safari versions', + 'last 2 edge versions', + ], + }, + }, + ], + '@babel/preset-react', + '@babel/preset-typescript', + ], + plugins: [ + '@babel/plugin-transform-runtime', + '@babel/plugin-proposal-class-properties', + '@babel/proposal-object-rest-spread', + '@babel/plugin-syntax-dynamic-import', + ], + overrides: [ + { + include: 'packages/fields/src/Controller.js', + presets: ['@babel/preset-env'], + }, + ], +}; diff --git a/examples/testing/example.test.ts b/examples/testing/example.test.ts new file mode 100644 index 00000000000..4e05884d617 --- /dev/null +++ b/examples/testing/example.test.ts @@ -0,0 +1,193 @@ +import { KeystoneContext } from '@keystone-next/types'; +import { setupTestEnv, setupTestRunner, TestEnv } from '@keystone-next/testing'; +import config from './keystone'; + +// Setup a test runner which will provide a clean test environment +// with access to our GraphQL API for each test. +const runner = setupTestRunner({ config }); + +describe('Example tests using test runner', () => { + test( + 'Create a Person using the list items API', + runner(async ({ context }) => { + // We can use the context argument provided by the test runner to access + // the full context API. + const person = await context.lists.Person.createOne({ + data: { name: 'Alice', email: 'alice@example.com', password: 'super-secret' }, + query: 'id name email password { isSet }', + }); + expect(person.name).toEqual('Alice'); + expect(person.email).toEqual('alice@example.com'); + expect(person.password.isSet).toEqual(true); + }) + ); + + test( + 'Create a Person using a hand-crafted GraphQL query sent over HTTP', + runner(async ({ graphQLRequest }) => { + // We can use the graphQLRequest argument provided by the test runner + // to execute HTTP requests to our GraphQL API and get a supertest + // "Test" object back. https://github.com/visionmedia/supertest + const { body } = await graphQLRequest({ + query: `mutation { + createPerson(data: { name: "Alice", email: "alice@example.com", password: "super-secret" }) { + id name email password { isSet } + } + }`, + }).expect(200); + const person = body.data.createPerson; + expect(person.name).toEqual('Alice'); + expect(person.email).toEqual('alice@example.com'); + expect(person.password.isSet).toEqual(true); + }) + ); + + test( + 'Check that trying to create user with no name (required field) fails', + runner(async ({ context }) => { + // The context.graphql.raw API is useful when we expect to recieve an + // error from an operation. + const { data, errors } = await context.graphql.raw({ + query: `mutation { + createPerson(data: { email: "alice@example.com", password: "super-secret" }) { + id name email password { isSet } + } + }`, + }); + expect(data!.createPerson).toBe(null); + expect(errors).toHaveLength(1); + expect(errors![0].path).toEqual(['createPerson']); + expect(errors![0].message).toEqual('You attempted to perform an invalid mutation'); + }) + ); + + test( + 'Check access control by running updateTask as a specific user via context.withSession()', + runner(async ({ context }) => { + // We can modify the value of context.session via context.withSession() to masquerade + // as different logged in users. This allows us to test that our access control rules + // are behaving as expected. + + // Create some users + const [alice, bob] = await context.lists.Person.createMany({ + data: [ + { data: { name: 'Alice', email: 'alice@example.com', password: 'super-secret' } }, + { data: { name: 'Bob', email: 'bob@example.com', password: 'super-secret' } }, + ], + query: 'id name', + }); + expect(alice.name).toEqual('Alice'); + expect(bob.name).toEqual('Bob'); + + // Create a task assigned to Alice + const task = await context.lists.Task.createOne({ + data: { + label: 'Experiment with Keystone', + priority: 'high', + isComplete: false, + assignedTo: { connect: { id: alice.id } }, + }, + query: 'id label priority isComplete assignedTo { name }', + }); + expect(task.label).toEqual('Experiment with Keystone'); + expect(task.priority).toEqual('high'); + expect(task.isComplete).toEqual(false); + expect(task.assignedTo.name).toEqual('Alice'); + + // Check that we can't update the task (not logged in) + { + const { data, errors } = await context.graphql.raw({ + query: `mutation update($id: ID!) { + updateTask(id: $id data: { isComplete: true }) { + id + } + }`, + variables: { id: task.id }, + }); + expect(data!.updateTask).toBe(null); + expect(errors).toHaveLength(1); + expect(errors![0].path).toEqual(['updateTask']); + expect(errors![0].message).toEqual('You do not have access to this resource'); + } + + { + // Check that we can update the task when logged in as Alice + const { data, errors } = await context + .withSession({ itemId: alice.id, data: {} }) + .graphql.raw({ + query: `mutation update($id: ID!) { + updateTask(id: $id data: { isComplete: true }) { + id + } + }`, + variables: { id: task.id }, + }); + expect(data!.updateTask.id).toEqual(task.id); + expect(errors).toBe(undefined); + } + + // Check that we can't update the task when logged in as Bob + { + const { data, errors } = await context + .withSession({ itemId: bob.id, data: {} }) + .graphql.raw({ + query: `mutation update($id: ID!) { + updateTask(id: $id data: { isComplete: true }) { + id + } + }`, + variables: { id: task.id }, + }); + expect(data!.updateTask).toBe(null); + expect(errors).toHaveLength(1); + expect(errors![0].path).toEqual(['updateTask']); + expect(errors![0].message).toEqual('You do not have access to this resource'); + } + }) + ); +}); + +describe('Example tests using test environment', () => { + // The test runner provided by setupTestRunner will drop all the data from the + // database and then provide a fresh connection for every test. + // + // If we want to use the same database for multiple tests, without deleting data + // between each test, we can use setupTestEnv in our `beforeAll()` block and + // manage the connection and disconnection ourselves. + // + // This gives us the opportunity to seed test data once up front and use it in + // multiple tests. + let testEnv: TestEnv, context: KeystoneContext; + let person: { id: string }; + beforeAll(async () => { + testEnv = await setupTestEnv({ config }); + context = testEnv.testArgs.context; + + await testEnv.connect(); + + // Create a person in the database to be used in multiple tests + person = (await context.lists.Person.createOne({ + data: { name: 'Alice', email: 'alice@example.com', password: 'super-secret' }, + })) as { id: string }; + }); + afterAll(async () => { + await testEnv.disconnect(); + }); + + test('Check that the persons password is set', async () => { + const { password } = await context.lists.Person.findOne({ + where: { id: person.id }, + query: 'password { isSet }', + }); + expect(password.isSet).toEqual(true); + }); + + test('Update the persons email address', async () => { + const { email } = await context.lists.Person.updateOne({ + id: person.id, + data: { email: 'new-email@example.com' }, + query: 'email', + }); + expect(email).toEqual('new-email@example.com'); + }); +}); diff --git a/examples/testing/keystone.ts b/examples/testing/keystone.ts new file mode 100644 index 00000000000..4c61b4cf511 --- /dev/null +++ b/examples/testing/keystone.ts @@ -0,0 +1,45 @@ +import { config } from '@keystone-next/keystone/schema'; +import { statelessSessions } from '@keystone-next/keystone/session'; +import { createAuth } from '@keystone-next/auth'; +import { lists } from './schema'; + +// createAuth configures signin functionality based on the config below. Note this only implements +// authentication, i.e signing in as an item using identity and secret fields in a list. Session +// management and access control are controlled independently in the main keystone config. +const { withAuth } = createAuth({ + // This is the list that contains items people can sign in as + listKey: 'Person', + // The identity field is typically a username or email address + identityField: 'email', + // The secret field must be a password type field + secretField: 'password', + + // initFirstItem turns on the "First User" experience, which prompts you to create a new user + // when there are no items in the list yet + initFirstItem: { + // These fields are collected in the "Create First User" form + fields: ['name', 'email', 'password'], + }, +}); + +// Stateless sessions will store the listKey and itemId of the signed-in user in a cookie. +// This session object will be made available on the context object used in hooks, access-control, +// resolvers, etc. +const session = statelessSessions({ + // The session secret is used to encrypt cookie data (should be an environment variable) + secret: '-- EXAMPLE COOKIE SECRET; CHANGE ME --', +}); + +// We wrap our config using the withAuth function. This will inject all +// the extra config required to add support for authentication in our system. +export default withAuth( + config({ + db: { + provider: 'sqlite', + url: process.env.DATABASE_URL || 'file:./keystone-example.db', + }, + lists, + // We add our session configuration to the system here. + session, + }) +); diff --git a/examples/testing/package.json b/examples/testing/package.json new file mode 100644 index 00000000000..b741a009a3d --- /dev/null +++ b/examples/testing/package.json @@ -0,0 +1,31 @@ +{ + "name": "@keystone-next/example-testing", + "version": "0.0.0", + "private": true, + "license": "MIT", + "scripts": { + "dev": "keystone-next dev", + "start": "keystone-next start", + "build": "keystone-next build", + "test": "jest --maxWorkers=1 --testTimeout=60000" + }, + "dependencies": { + "@babel/core": "^7.14.6", + "@babel/preset-env": "^7.14.5", + "@babel/preset-typescript": "^7.14.5", + "@keystone-next/auth": "^27.0.0", + "@keystone-next/fields": "^11.0.0", + "@keystone-next/keystone": "^20.0.0", + "@keystone-next/testing": "^0.0.0", + "@keystone-next/types": "^20.0.0" + }, + "devDependencies": { + "babel-jest": "^26.6.3", + "jest": "^27.0.4", + "typescript": "^4.3.4" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "repository": "https://github.com/keystonejs/keystone/tree/master/examples/testing" +} diff --git a/examples/testing/schema.graphql b/examples/testing/schema.graphql new file mode 100644 index 00000000000..f2930768ce3 --- /dev/null +++ b/examples/testing/schema.graphql @@ -0,0 +1,507 @@ +""" + A keystone list +""" +type Task { + id: ID! + label: String + priority: TaskPriorityType + isComplete: Boolean + assignedTo: Person + finishBy: String +} + +enum TaskPriorityType { + low + medium + high +} + +input TaskWhereInput { + AND: [TaskWhereInput!] + OR: [TaskWhereInput!] + id: ID + id_not: ID + id_lt: ID + id_lte: ID + id_gt: ID + id_gte: ID + id_in: [ID] + id_not_in: [ID] + label: String + label_not: String + label_contains: String + label_not_contains: String + label_in: [String] + label_not_in: [String] + priority: TaskPriorityType + priority_not: TaskPriorityType + priority_in: [TaskPriorityType] + priority_not_in: [TaskPriorityType] + isComplete: Boolean + isComplete_not: Boolean + assignedTo: PersonWhereInput + assignedTo_is_null: Boolean + finishBy: String + finishBy_not: String + finishBy_lt: String + finishBy_lte: String + finishBy_gt: String + finishBy_gte: String + finishBy_in: [String] + finishBy_not_in: [String] +} + +input TaskWhereUniqueInput { + id: ID +} + +enum SortTasksBy { + id_ASC + id_DESC + label_ASC + label_DESC + priority_ASC + priority_DESC + isComplete_ASC + isComplete_DESC + finishBy_ASC + finishBy_DESC +} + +input TaskOrderByInput { + id: OrderDirection + label: OrderDirection + priority: OrderDirection + isComplete: OrderDirection + finishBy: OrderDirection +} + +enum OrderDirection { + asc + desc +} + +input TaskUpdateInput { + label: String + priority: TaskPriorityType + isComplete: Boolean + assignedTo: PersonRelateToOneInput + finishBy: String +} + +input PersonRelateToOneInput { + create: PersonCreateInput + connect: PersonWhereUniqueInput + disconnect: PersonWhereUniqueInput + disconnectAll: Boolean +} + +input TasksUpdateInput { + id: ID! + data: TaskUpdateInput +} + +input TaskCreateInput { + label: String + priority: TaskPriorityType + isComplete: Boolean + assignedTo: PersonRelateToOneInput + finishBy: String +} + +input TasksCreateInput { + data: TaskCreateInput +} + +""" + A keystone list +""" +type Person { + id: ID! + name: String + email: String + password: PasswordState + tasks( + where: TaskWhereInput! = {} + search: String + sortBy: [SortTasksBy!] + @deprecated(reason: "sortBy has been deprecated in favour of orderBy") + orderBy: [TaskOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): [Task!] + _tasksMeta( + where: TaskWhereInput! = {} + search: String + sortBy: [SortTasksBy!] + @deprecated(reason: "sortBy has been deprecated in favour of orderBy") + orderBy: [TaskOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): _QueryMeta + @deprecated( + reason: "This query will be removed in a future version. Please use tasksCount instead." + ) + tasksCount(where: TaskWhereInput! = {}): Int +} + +type PasswordState { + isSet: Boolean! +} + +type _QueryMeta { + count: Int +} + +input PersonWhereInput { + AND: [PersonWhereInput!] + OR: [PersonWhereInput!] + id: ID + id_not: ID + id_lt: ID + id_lte: ID + id_gt: ID + id_gte: ID + id_in: [ID] + id_not_in: [ID] + name: String + name_not: String + name_contains: String + name_not_contains: String + name_in: [String] + name_not_in: [String] + email: String + email_not: String + email_contains: String + email_not_contains: String + email_in: [String] + email_not_in: [String] + password_is_set: Boolean + + """ + condition must be true for all nodes + """ + tasks_every: TaskWhereInput + + """ + condition must be true for at least 1 node + """ + tasks_some: TaskWhereInput + + """ + condition must be false for all nodes + """ + tasks_none: TaskWhereInput +} + +input PersonWhereUniqueInput { + id: ID + email: String +} + +enum SortPeopleBy { + id_ASC + id_DESC + name_ASC + name_DESC + email_ASC + email_DESC +} + +input PersonOrderByInput { + id: OrderDirection + name: OrderDirection + email: OrderDirection +} + +input PersonUpdateInput { + name: String + email: String + password: String + tasks: TaskRelateToManyInput +} + +input TaskRelateToManyInput { + create: [TaskCreateInput] + connect: [TaskWhereUniqueInput] + disconnect: [TaskWhereUniqueInput] + disconnectAll: Boolean +} + +input PeopleUpdateInput { + id: ID! + data: PersonUpdateInput +} + +input PersonCreateInput { + name: String + email: String + password: String + tasks: TaskRelateToManyInput +} + +input PeopleCreateInput { + data: PersonCreateInput +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON + @specifiedBy( + url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf" + ) + +type Mutation { + """ + Create a single Task item. + """ + createTask(data: TaskCreateInput): Task + + """ + Create multiple Task items. + """ + createTasks(data: [TasksCreateInput]): [Task] + + """ + Update a single Task item by ID. + """ + updateTask(id: ID!, data: TaskUpdateInput): Task + + """ + Update multiple Task items by ID. + """ + updateTasks(data: [TasksUpdateInput]): [Task] + + """ + Delete a single Task item by ID. + """ + deleteTask(id: ID!): Task + + """ + Delete multiple Task items by ID. + """ + deleteTasks(ids: [ID!]): [Task] + + """ + Create a single Person item. + """ + createPerson(data: PersonCreateInput): Person + + """ + Create multiple Person items. + """ + createPeople(data: [PeopleCreateInput]): [Person] + + """ + Update a single Person item by ID. + """ + updatePerson(id: ID!, data: PersonUpdateInput): Person + + """ + Update multiple Person items by ID. + """ + updatePeople(data: [PeopleUpdateInput]): [Person] + + """ + Delete a single Person item by ID. + """ + deletePerson(id: ID!): Person + + """ + Delete multiple Person items by ID. + """ + deletePeople(ids: [ID!]): [Person] + authenticatePersonWithPassword( + email: String! + password: String! + ): PersonAuthenticationWithPasswordResult! + createInitialPerson( + data: CreateInitialPersonInput! + ): PersonAuthenticationWithPasswordSuccess! + endSession: Boolean! +} + +union AuthenticatedItem = Person + +union PersonAuthenticationWithPasswordResult = + PersonAuthenticationWithPasswordSuccess + | PersonAuthenticationWithPasswordFailure + +type PersonAuthenticationWithPasswordSuccess { + sessionToken: String! + item: Person! +} + +type PersonAuthenticationWithPasswordFailure { + code: PasswordAuthErrorCode! + message: String! +} + +enum PasswordAuthErrorCode { + FAILURE + IDENTITY_NOT_FOUND + SECRET_NOT_SET + MULTIPLE_IDENTITY_MATCHES + SECRET_MISMATCH +} + +input CreateInitialPersonInput { + name: String + email: String + password: String +} + +type Query { + """ + Search for all Task items which match the where clause. + """ + allTasks( + where: TaskWhereInput! = {} + search: String + sortBy: [SortTasksBy!] + @deprecated(reason: "sortBy has been deprecated in favour of orderBy") + orderBy: [TaskOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): [Task!] + + """ + Search for the Task item with the matching ID. + """ + Task(where: TaskWhereUniqueInput!): Task + + """ + Perform a meta-query on all Task items which match the where clause. + """ + _allTasksMeta( + where: TaskWhereInput! = {} + search: String + sortBy: [SortTasksBy!] + @deprecated(reason: "sortBy has been deprecated in favour of orderBy") + orderBy: [TaskOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): _QueryMeta + @deprecated( + reason: "This query will be removed in a future version. Please use tasksCount instead." + ) + tasksCount(where: TaskWhereInput! = {}): Int + + """ + Search for all Person items which match the where clause. + """ + allPeople( + where: PersonWhereInput! = {} + search: String + sortBy: [SortPeopleBy!] + @deprecated(reason: "sortBy has been deprecated in favour of orderBy") + orderBy: [PersonOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): [Person!] + + """ + Search for the Person item with the matching ID. + """ + Person(where: PersonWhereUniqueInput!): Person + + """ + Perform a meta-query on all Person items which match the where clause. + """ + _allPeopleMeta( + where: PersonWhereInput! = {} + search: String + sortBy: [SortPeopleBy!] + @deprecated(reason: "sortBy has been deprecated in favour of orderBy") + orderBy: [PersonOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): _QueryMeta + @deprecated( + reason: "This query will be removed in a future version. Please use peopleCount instead." + ) + peopleCount(where: PersonWhereInput! = {}): Int + authenticatedItem: AuthenticatedItem + keystone: KeystoneMeta! +} + +type KeystoneMeta { + adminMeta: KeystoneAdminMeta! +} + +type KeystoneAdminMeta { + enableSignout: Boolean! + enableSessionItem: Boolean! + lists: [KeystoneAdminUIListMeta!]! + list(key: String!): KeystoneAdminUIListMeta +} + +type KeystoneAdminUIListMeta { + key: String! + itemQueryName: String! + listQueryName: String! + hideCreate: Boolean! + hideDelete: Boolean! + path: String! + label: String! + singular: String! + plural: String! + description: String + initialColumns: [String!]! + pageSize: Int! + labelField: String! + fields: [KeystoneAdminUIFieldMeta!]! + initialSort: KeystoneAdminUISort + isHidden: Boolean! +} + +type KeystoneAdminUIFieldMeta { + path: String! + label: String! + isOrderable: Boolean! + fieldMeta: JSON + viewsIndex: Int! + customViewsIndex: Int + createView: KeystoneAdminUIFieldMetaCreateView! + listView: KeystoneAdminUIFieldMetaListView! + itemView(id: ID!): KeystoneAdminUIFieldMetaItemView +} + +type KeystoneAdminUIFieldMetaCreateView { + fieldMode: KeystoneAdminUIFieldMetaCreateViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaCreateViewFieldMode { + edit + hidden +} + +type KeystoneAdminUIFieldMetaListView { + fieldMode: KeystoneAdminUIFieldMetaListViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaListViewFieldMode { + read + hidden +} + +type KeystoneAdminUIFieldMetaItemView { + fieldMode: KeystoneAdminUIFieldMetaItemViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaItemViewFieldMode { + edit + read + hidden +} + +type KeystoneAdminUISort { + field: String! + direction: KeystoneAdminUISortDirection! +} + +enum KeystoneAdminUISortDirection { + ASC + DESC +} diff --git a/examples/testing/schema.prisma b/examples/testing/schema.prisma new file mode 100644 index 00000000000..089eae08d28 --- /dev/null +++ b/examples/testing/schema.prisma @@ -0,0 +1,29 @@ +datasource sqlite { + url = env("DATABASE_URL") + provider = "sqlite" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.prisma/client" +} + +model Task { + id Int @id @default(autoincrement()) + label String? + priority String? + isComplete Boolean? + assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) + assignedToId Int? @map("assignedTo") + finishBy DateTime? + + @@index([assignedToId]) +} + +model Person { + id Int @id @default(autoincrement()) + name String? + email String? @unique + password String? + tasks Task[] @relation("Task_assignedTo") +} \ No newline at end of file diff --git a/examples/testing/schema.ts b/examples/testing/schema.ts new file mode 100644 index 00000000000..3949439b06c --- /dev/null +++ b/examples/testing/schema.ts @@ -0,0 +1,41 @@ +import { createSchema, list } from '@keystone-next/keystone/schema'; +import { checkbox, password, relationship, text, timestamp } from '@keystone-next/fields'; +import { select } from '@keystone-next/fields'; + +export const lists = createSchema({ + Task: list({ + fields: { + label: text({ isRequired: true }), + priority: select({ + dataType: 'enum', + options: [ + { label: 'Low', value: 'low' }, + { label: 'Medium', value: 'medium' }, + { label: 'High', value: 'high' }, + ], + }), + isComplete: checkbox(), + assignedTo: relationship({ ref: 'Person.tasks', many: false }), + finishBy: timestamp(), + }, + // Add access control so that only the assigned user can update a task + // We will write a test to verify that this is working correctly. + access: { + update: async ({ session, itemId, context }) => { + const task = await context.lists.Task.findOne({ + where: { id: itemId }, + query: 'assignedTo { id }', + }); + return !!(session?.itemId && session.itemId === task.assignedTo?.id); + }, + }, + }), + Person: list({ + fields: { + name: text({ isRequired: true }), + email: text({ isRequired: true, isUnique: true }), + password: password({ isRequired: true }), + tasks: relationship({ ref: 'Task.assignedTo', many: true }), + }, + }), +}); diff --git a/tests/examples-smoke-tests/testing.test.ts b/tests/examples-smoke-tests/testing.test.ts new file mode 100644 index 00000000000..d414e935f26 --- /dev/null +++ b/tests/examples-smoke-tests/testing.test.ts @@ -0,0 +1,16 @@ +import { Browser, Page } from 'playwright'; +import { exampleProjectTests, initFirstItemTest } from './utils'; + +exampleProjectTests('testing', browserType => { + let browser: Browser = undefined as any; + let page: Page = undefined as any; + beforeAll(async () => { + browser = await browserType.launch(); + page = await browser.newPage(); + page.goto('http://localhost:3000'); + }); + initFirstItemTest(() => page); + afterAll(async () => { + await browser.close(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 3a5ef644730..397ed36a379 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1333,6 +1333,14 @@ human-id "^1.0.2" prettier "^1.19.1" +"@cnakazawa/watch@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" + integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + "@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" @@ -1706,6 +1714,27 @@ jest-haste-map "^27.0.2" jest-runtime "^27.0.4" +"@jest/transform@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" + integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^26.6.2" + babel-plugin-istanbul "^6.0.0" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.4" + jest-haste-map "^26.6.2" + jest-regex-util "^26.0.0" + jest-util "^26.6.2" + micromatch "^4.0.2" + pirates "^4.0.1" + slash "^3.0.0" + source-map "^0.6.1" + write-file-atomic "^3.0.0" + "@jest/transform@^27.0.2": version "27.0.2" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.0.2.tgz#b073b7c589e3f4b842102468875def2bb722d6b5" @@ -2501,7 +2530,7 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b" integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14", "@types/babel__core@^7.1.7": version "7.1.14" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" integrity sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g== @@ -3916,6 +3945,20 @@ axios@^0.21.1: dependencies: follow-redirects "^1.10.0" +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== + dependencies: + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" + "@types/babel__core" "^7.1.7" + babel-plugin-istanbul "^6.0.0" + babel-preset-jest "^26.6.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + slash "^3.0.0" + babel-jest@^27.0.2: version "27.0.2" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.0.2.tgz#7dc18adb01322acce62c2af76ea2c7cd186ade37" @@ -3963,6 +4006,16 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + babel-plugin-jest-hoist@^27.0.1: version "27.0.1" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.0.1.tgz#a6d10e484c93abff0f4e95f437dad26e5736ea11" @@ -4025,6 +4078,14 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== + dependencies: + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" + babel-preset-jest@^27.0.1: version "27.0.1" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.0.1.tgz#7a50c75d16647c23a2cf5158d5bb9eb206b10e20" @@ -4478,6 +4539,13 @@ caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.300012 resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz#66e8669985bb2cb84ccb10f68c25ce6dd3e4d2b8" integrity sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ== +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -5117,6 +5185,17 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -6145,6 +6224,11 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +exec-sh@^0.3.2: + version "0.3.6" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" + integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w== + execa@5.1.1, execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -6173,6 +6257,19 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" @@ -6790,7 +6887,7 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@^2.3.2, fsevents@~2.3.1, fsevents@~2.3.2: +fsevents@^2.1.2, fsevents@^2.3.2, fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -6851,7 +6948,7 @@ get-stream@^3.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= -get-stream@^4.1.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== @@ -8288,6 +8385,27 @@ jest-get-type@^27.0.1: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.1.tgz#34951e2b08c8801eb28559d7eb732b04bbcf7815" integrity sha512-9Tggo9zZbu0sHKebiAijyt1NM77Z0uO4tuWOxUCujAiSeXv30Vb5D4xVF4UR4YWNapcftj+PbByU54lKD7/xMg== +jest-haste-map@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" + integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== + dependencies: + "@jest/types" "^26.6.2" + "@types/graceful-fs" "^4.1.2" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.4" + jest-regex-util "^26.0.0" + jest-serializer "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" + micromatch "^4.0.2" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.1.2" + jest-haste-map@^27.0.2: version "27.0.2" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.0.2.tgz#3f1819400c671237e48b4d4b76a80a0dbed7577f" @@ -8378,6 +8496,11 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== +jest-regex-util@^26.0.0: + version "26.0.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" + integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== + jest-regex-util@^27.0.1: version "27.0.1" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.1.tgz#69d4b1bf5b690faa3490113c47486ed85dd45b68" @@ -8467,6 +8590,14 @@ jest-runtime@^27.0.4: strip-bom "^4.0.0" yargs "^16.0.3" +jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.4" + jest-serializer@^27.0.1: version "27.0.1" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.0.1.tgz#2464d04dcc33fb71dc80b7c82e3c5e8a08cb1020" @@ -8505,6 +8636,18 @@ jest-snapshot@^27.0.4: pretty-format "^27.0.2" semver "^7.3.2" +jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== + dependencies: + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + jest-util@^27.0.2: version "27.0.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.0.2.tgz#fc2c7ace3c75ae561cf1e5fdb643bf685a5be7c7" @@ -8551,7 +8694,7 @@ jest-worker@27.0.0-next.5: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^26.3.0: +jest-worker@^26.3.0, jest-worker@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== @@ -9556,7 +9699,7 @@ minimist-options@^3.0.1: arrify "^1.0.1" is-plain-obj "^1.1.0" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -9744,6 +9887,11 @@ next@^10.2.3: vm-browserify "1.1.2" watchpack "2.1.1" +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -10367,7 +10515,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-key@^2.0.0: +path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= @@ -11500,6 +11648,11 @@ rollup@^2.32.0: optionalDependencies: fsevents "~2.3.2" +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -11536,6 +11689,21 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -11580,7 +11748,7 @@ sembear@^0.5.0: "@types/semver" "^6.0.1" semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@^5.4.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -13532,7 +13700,7 @@ wait-on@5.3.0: minimist "^1.2.5" rxjs "^6.6.3" -walker@^1.0.7: +walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=