diff --git a/.gitignore b/.gitignore index 67446de6427a..3087f9c587a7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ packages/server/test/support/fixtures/server/libs # graphql, auto-generated /packages/launchpad/src/generated +/packages/app/src/generated # from npm/create-cypress-tests /npm/create-cypress-tests/initial-template @@ -343,3 +344,7 @@ $RECYCLE.BIN/ # Circle cache artifacts globbed_node_modules + +# Autogenerated files, typically from graphql-code-generator +*.gen.ts +*.gen.json \ No newline at end of file diff --git a/apollo.config.js b/apollo.config.js index 0cf492729f25..15cf81dbab92 100644 --- a/apollo.config.js +++ b/apollo.config.js @@ -1,12 +1,14 @@ +const path = require('path') + // For use with Apollo Extension for VSCode: // https://www.apollographql.com/docs/devtools/editor-plugins/ module.exports = { client: { service: { name: 'cypress-io', - localSchemaFile: './packages/graphql/schema.graphql', + localSchemaFile: path.join(__dirname, 'packages/graphql/schemas/schema.graphql'), }, tagName: 'gql', - includes: ['./packages/launchpad/src/**/*.vue'], + includes: [path.join(__dirname, 'packages/launchpad/src/**/*.vue')], }, } diff --git a/autobarrel.json b/autobarrel.json index 819ccb63e1f1..7150f45a5329 100644 --- a/autobarrel.json +++ b/autobarrel.json @@ -2,5 +2,10 @@ "prefix": "/* eslint-disable padding-line-between-statements */", "paths": [ "packages/graphql/src/**/*" + ], + "ignore": [ + "packages/graphql/src/stitching", + "packages/graphql/src/testing", + "packages/graphql/src/gen" ] } \ No newline at end of file diff --git a/circle.yml b/circle.yml index 8a6866bf1f4a..e7147bd1b81c 100644 --- a/circle.yml +++ b/circle.yml @@ -117,6 +117,9 @@ commands: build-and-persist: description: Save entire folder as artifact for other jobs to run without reinstalling steps: + - run: + name: Build all codegen + command: yarn gulp buildProd - run: name: Build packages command: yarn build @@ -389,8 +392,11 @@ commands: type: string steps: - restore_cached_workspace - - run: yarn gulp graphqlCodegen - - run: yarn workspace @packages/launchpad cypress:run --browser <> + - run: + command: | + CYPRESS_KONFIG_ENV=production \ + CYPRESS_RECORD_KEY=$TEST_LAUNCHPAD_RECORD_KEY \ + yarn workspace @packages/launchpad cypress:run --browser <> --record --parallel - store_test_results: path: /tmp/cypress - store_artifacts: @@ -934,6 +940,16 @@ jobs: command: node cli/bin/cypress info --dev - store-npm-logs + check-ts: + <<: *defaults + steps: + - restore_cached_workspace + - install-required-node + - run: + name: Check TS Types + command: yarn gulp checkTs + + # a special job that keeps polling Circle and when all # individual jobs are finished, it closes the Percy build percy-finalize: @@ -1002,8 +1018,6 @@ jobs: - run: yarn test-scripts # make sure our snapshots are compared correctly - run: yarn test-mocha-snapshot - # Get the codegen output - - run: yarn gulp buildProd # make sure packages with TypeScript can be transpiled to JS - run: yarn lerna run build-prod --stream # run unit tests from each individual package @@ -1153,7 +1167,7 @@ jobs: run-launchpad-integration-tests-chrome: <<: *defaults - parallelism: 1 + parallelism: 3 steps: - run-launchpad-integration-tests: browser: chrome @@ -1981,6 +1995,9 @@ linux-workflow: &linux-workflow - build: requires: - node_modules_install + - check-ts: + requires: + - build - lint: name: Linux lint requires: @@ -2047,6 +2064,7 @@ linux-workflow: &linux-workflow requires: - build - run-launchpad-integration-tests-chrome: + context: test-runner:launchpad-tests requires: - build @@ -2105,6 +2123,7 @@ linux-workflow: &linux-workflow context: test-runner:npm-release requires: - build + - check-ts - npm-eslint-plugin-dev - npm-create-cypress-tests - npm-react diff --git a/cli/lib/exec/spawn.js b/cli/lib/exec/spawn.js index d014dbb964a6..60ab862cbc32 100644 --- a/cli/lib/exec/spawn.js +++ b/cli/lib/exec/spawn.js @@ -118,6 +118,9 @@ module.exports = { } // strip dev out of child process options + /** + * @type {import('child_process').ForkOptions} + */ let stdioOptions = _.pick(options, 'env', 'detached', 'stdio') // figure out if we're going to be force enabling or disabling colors. @@ -154,7 +157,12 @@ module.exports = { let child if (process.env.CYPRESS_INTERNAL_DEV_WATCH) { + if (process.env.CYPRESS_INTERNAL_DEV_DEBUG) { + stdioOptions.execArgv = [process.env.CYPRESS_INTERNAL_DEV_DEBUG] + } + debug('spawning Cypress as fork: %s', startScriptPath) + child = cp.fork(startScriptPath, args, stdioOptions) process.on('message', (msg) => { child.send(msg) diff --git a/graphql-codegen.yml b/graphql-codegen.yml index 7c00edbd7a1d..99412e281687 100644 --- a/graphql-codegen.yml +++ b/graphql-codegen.yml @@ -1,37 +1,93 @@ +# https://www.graphql-code-generator.com/docs/getting-started/index + +documentFilters: &documentFilters + immutableTypes: true + useTypeImports: true + preResolveTypes: true + onlyOperationTypes: true + avoidOptionals: true + strictScalars: true + scalars: + Date: string + DateTime: string + JSON: any + +vueOperations: &vueOperations + schema: './packages/graphql/schemas/schema.graphql' + config: + <<: *documentFilters + plugins: + - add: + content: '/* eslint-disable */' + - 'typescript' + - 'typescript-operations' + - 'typed-document-node': + # Intentionally specified under typed-document-node rather than top level config, + # becuase we don't want it flattening the types for the operations + flattenGeneratedTypes: true + +vueTesting: &vueTesting + schema: './packages/graphql/schemas/schema.graphql' + config: + <<: *documentFilters + plugins: + - add: + content: '/* eslint-disable */' + - 'typescript' + - 'typescript-operations' + - 'typed-document-node' + overwrite: true -schema: './packages/graphql/schema.graphql' +config: + enumsAsTypes: true + declarationKind: 'interface' generates: - './packages/app/src/generated/graphql.ts': - documents: './packages/app/src/**/*.vue' - config: - immutableTypes: true - useTypeImports: true - preResolveTypes: true - onlyOperationTypes: true - avoidOptionals: true - enumsAsTypes: true + ### + # Generates types for us to infer the correct "source types" when we merge the + # remote schema cloud.graphql schema into Nexus. This ensures we have proper type checking + # when we're using cy.mountFragment in component tests + ### + './packages/graphql/src/gen/cloud-source-types.gen.ts': + schema: 'packages/graphql/schemas/cloud.graphql' plugins: - add: content: '/* eslint-disable */' - 'typescript': - - 'typescript-operations' - - 'typed-document-node' + nonOptionalTypename: true + - 'packages/graphql/script/codegen-type-map.js' + + ### + # All of the GraphQL Query/Mutation documents we import for use in the .vue + # files for useQuery / useMutation, as well as types associated with the fragments + ### './packages/launchpad/src/generated/graphql.ts': documents: './packages/launchpad/src/**/*.vue' - config: - immutableTypes: true - useTypeImports: true - preResolveTypes: true - onlyOperationTypes: true - avoidOptionals: true - enumsAsTypes: true - plugins: - - add: - content: '/* eslint-disable */' - - 'typescript': - - 'typescript-operations' - - 'typed-document-node' + <<: *vueOperations + + './packages/app/src/generated/graphql.ts': + documents: './packages/app/src/**/*.vue' + <<: *vueOperations + + ### + # All GraphQL documents imported into the .spec.tsx files for component testing. + # Similar to generated/graphql.ts, except it doesn't include the flattening for the document nodes, + # so we can actually use the document in cy.mountFragment + ### + './packages/launchpad/src/generated/graphql-test.ts': + documents: './packages/launchpad/src/**/*.vue' + <<: *vueTesting + + './packages/app/src/generated/graphql-test.ts': + documents: './packages/app/src/**/*.vue' + <<: *vueTesting + + ### + # A Custom GraphQL Code generator (codegen-mount.js), producing a GraphQL type which represents the "union" + # of all possible output types. This is exposed as `testFragmentMember` / `testFragmentMemberList` + # fields, and are used in testing components, so we can generate a type that fulfills a fragment + ### './packages/graphql/src/testing/testUnionType.ts': + schema: './packages/graphql/schemas/schema.graphql' plugins: - add: content: '/* eslint-disable */' diff --git a/npm/eslint-plugin-dev/lib/index.js b/npm/eslint-plugin-dev/lib/index.js index 47beefa8f0a9..fbbc0f80661f 100644 --- a/npm/eslint-plugin-dev/lib/index.js +++ b/npm/eslint-plugin-dev/lib/index.js @@ -116,7 +116,7 @@ const baseRules = { 'no-unneeded-ternary': 'error', 'no-unreachable': 'error', 'no-unused-labels': 'error', - 'no-unused-vars': ['error', { args: 'none' }], + 'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }], 'no-useless-concat': 'error', 'no-useless-constructor': 'error', 'no-var': 'error', @@ -269,10 +269,13 @@ module.exports = { rules: { 'no-undef': 'off', 'no-unused-vars': 'off', + 'no-useless-constructor': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { 'args': 'none', + 'ignoreRestSiblings': true, + 'argsIgnorePattern': '^_', }, ], '@typescript-eslint/type-annotation-spacing': 'error', diff --git a/npm/mount-utils/package.json b/npm/mount-utils/package.json index 271d0b257159..28bb18d407cb 100644 --- a/npm/mount-utils/package.json +++ b/npm/mount-utils/package.json @@ -4,8 +4,9 @@ "description": "Shared utilities for the various component testing adapters", "main": "dist/index.js", "scripts": { - "build": "tsc", - "build-prod": "tsc", + "build": "tsc || echo 'built, with type errors'", + "build-prod": "tsc || echo 'built, with type errors'", + "check-ts": "tsc --noEmit", "watch": "tsc -w" }, "dependencies": {}, diff --git a/npm/vite-dev-server/package.json b/npm/vite-dev-server/package.json index a30aeb8add9c..1ed802cbef42 100644 --- a/npm/vite-dev-server/package.json +++ b/npm/vite-dev-server/package.json @@ -4,8 +4,9 @@ "description": "Launches Vite Dev Server for Component Testing", "main": "index.js", "scripts": { - "build": "tsc", - "build-prod": "tsc", + "build": "tsc || echo 'built, with type errors'", + "build-prod": "tsc || echo 'built, with type errors'", + "check-ts": "tsc --noEmit", "cy:open": "node ../../scripts/cypress.js open-ct --project ${PWD}", "cy:run": "node ../../scripts/cypress.js run-ct --project ${PWD}", "test": "yarn cy:run", diff --git a/npm/vite-dev-server/vite.config.ts b/npm/vite-dev-server/vite.config.ts index 7c660f5826d8..96576bc14b9d 100644 --- a/npm/vite-dev-server/vite.config.ts +++ b/npm/vite-dev-server/vite.config.ts @@ -5,4 +5,7 @@ export default defineConfig({ plugins: [ vue(), ], + define: { + 'process.env': {}, + }, }) diff --git a/npm/vue/package.json b/npm/vue/package.json index e25095d66247..361463018b4b 100644 --- a/npm/vue/package.json +++ b/npm/vue/package.json @@ -53,7 +53,7 @@ "vue-loader": "16.1.2", "vue-router": "^4.0.0", "vue-style-loader": "4.1.2", - "vue-tsc": "0.2.2", + "vue-tsc": "^0.3.0", "vuex": "^4.0.0", "webpack": "4.42.0" }, diff --git a/npm/webpack-dev-server/package.json b/npm/webpack-dev-server/package.json index 6f8643edce90..3bcd94d74888 100644 --- a/npm/webpack-dev-server/package.json +++ b/npm/webpack-dev-server/package.json @@ -4,8 +4,9 @@ "description": "Launches Webpack Dev Server for Component Testing", "main": "dist/index.js", "scripts": { - "build": "tsc", - "build-prod": "tsc", + "build": "tsc || echo 'built, with type errors'", + "build-prod": "tsc || echo 'built, with type errors'", + "check-ts": "tsc --noEmit", "test": "node ./test-wds-3.js", "test-all": "tsc && mocha -r @packages/ts/register test/**/*.spec.ts test/*.spec.ts --exit", "watch": "tsc -w" diff --git a/npm/webpack-preprocessor/package.json b/npm/webpack-preprocessor/package.json index 59e8ea31e898..a080748b962b 100644 --- a/npm/webpack-preprocessor/package.json +++ b/npm/webpack-preprocessor/package.json @@ -5,7 +5,7 @@ "private": false, "main": "dist", "scripts": { - "build": "shx rm -rf dist && tsc", + "build": "shx rm -rf dist && tsc || echo 'built, with errors'", "build-prod": "yarn build", "deps": "deps-ok && dependency-check --no-dev .", "secure": "nsp check", @@ -16,7 +16,7 @@ "test-e2e": "mocha test/e2e/*.spec.*", "test-unit": "mocha test/unit/*.spec.*", "test-watch": "yarn test-unit & chokidar '**/*.(js|ts)' 'test/unit/*.(js|ts)' -c 'yarn test-unit'", - "types": "tsc --noEmit", + "check-ts": "tsc --noEmit", "watch": "yarn build --watch" }, "dependencies": { diff --git a/package.json b/package.json index c93d349375ed..56a1297bbf6c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "description": "Cypress.io end to end testing tool", "private": true, "scripts": { - "prebinary-build": "yarn gulp buildProd", "binary-build": "node ./scripts/binary.js build", "binary-deploy": "node ./scripts/binary.js deploy", "binary-deploy-linux": "./scripts/build-linux-binary.sh", @@ -14,12 +13,12 @@ "binary-upload": "node ./scripts/binary.js upload", "binary-zip": "node ./scripts/binary.js zip", "build": "lerna run build --stream --no-bail --ignore create-cypress-tests && lerna run build --stream --scope create-cypress-tests", - "prebuild-prod": "yarn gulp buildProd", "build-prod": "lerna run build-prod --stream --ignore create-cypress-tests && lerna run build-prod --stream --scope create-cypress-tests", "bump": "node ./scripts/binary.js bump", "check-node-version": "node scripts/check-node-version.js", "check-terminal": "node scripts/check-terminal.js", "clean": "lerna run clean --parallel", + "check-ts": "yarn gulp checkTs", "clean-deps": "find . -depth -name node_modules -type d -exec rm -rf {} \\;", "clean-untracked-files": "git clean -d -f", "precypress:open": "yarn ensure-deps", @@ -118,6 +117,7 @@ "@types/through2": "^2.0.36", "@typescript-eslint/eslint-plugin": "4.18.0", "@typescript-eslint/parser": "4.18.0", + "@urql/introspection": "^0.3.0", "ansi-styles": "3.2.1", "arg": "4.1.2", "ascii-table": "0.0.9", diff --git a/packages/app/package.json b/packages/app/package.json index 74014b6a1a10..4b0332d26aa9 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -3,7 +3,7 @@ "version": "0.0.0-development", "private": true, "scripts": { - "types": "vue-tsc --noEmit", + "check-ts": "vue-tsc --noEmit", "build-prod": "cross-env NODE_ENV=production vite build", "clean": "rm -rf dist && rm -rf ./node_modules/.vite && echo 'cleaned'", "clean-deps": "rm -rf node_modules", @@ -24,8 +24,7 @@ "@iconify/vue": "3.0.0-beta.1", "@intlify/vite-plugin-vue-i18n": "2.4.0", "@testing-library/cypress": "8.0.0", - "@urql/core": "2.1.5", - "@urql/exchange-execute": "^1.0.4", + "@urql/core": "2.3.1", "@urql/vue": "0.4.3", "@vitejs/plugin-vue": "1.2.4", "@vitejs/plugin-vue-jsx": "1.1.6", diff --git a/packages/app/src/Foo.spec.tsx b/packages/app/src/Foo.spec.tsx index 00577e83741b..38f30812c581 100644 --- a/packages/app/src/Foo.spec.tsx +++ b/packages/app/src/Foo.spec.tsx @@ -1,4 +1,4 @@ -import { FooFragmentDoc } from './generated/graphql' +import { FooFragmentDoc } from './generated/graphql-test' import Foo from './Foo.vue' describe('Foo', () => { diff --git a/packages/app/src/generated/graphql.ts b/packages/app/src/generated/graphql.ts deleted file mode 100644 index d2a0e131e1ff..000000000000 --- a/packages/app/src/generated/graphql.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable */ -import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; -export type Maybe = T | null; -export type Exact = { [K in keyof T]: T[K] }; -export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: string; - String: string; - Boolean: boolean; - Int: number; - Float: number; - /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ - DateTime: any; - /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ - JSON: any; -}; - - - -export type BrowserFamily = - | 'chromium' - | 'firefox'; - - - -export type FrontendFramework = - | 'cra' - | 'nextjs' - | 'nuxtjs' - | 'react' - | 'vue' - | 'vuecli'; - - - - -export type NavItem = - | 'learn' - | 'projectSetup' - | 'runs' - | 'settings'; - - - -export type PluginsState = - | 'error' - | 'initialized' - | 'initializing' - | 'uninitialized'; - - - - - -export type ResolvedConfigOption = - | 'config' - | 'default' - | 'env' - | 'plugin' - | 'runtime'; - - - - - - -export type ResolvedType = - | 'array' - | 'boolean' - | 'json' - | 'number' - | 'string'; - - - -export type RunGroupStatus = - | 'cancelled' - | 'errored' - | 'failed' - | 'noTests' - | 'passed' - | 'running' - | 'timedOut' - | 'unclaimed'; - -/** The bundlers that we can use with Cypress */ -export type SupportedBundlers = - | 'vite' - | 'webpack'; - -export type TestingTypeEnum = - | 'component' - | 'e2e'; - - - - - -export type WizardCodeLanguage = - | 'js' - | 'ts'; - - -export type WizardNavigateDirection = - | 'back' - | 'forward'; - - -export type WizardStep = - | 'createConfig' - | 'initializePlugins' - | 'installDependencies' - | 'selectFramework' - | 'setupComplete' - | 'welcome'; - -export type AppQueryVariables = Exact<{ [key: string]: never; }>; - - -export type AppQuery = { readonly __typename?: 'Query', readonly app: ( - { readonly __typename?: 'App' } - & FooFragment - ) }; - -export type FooFragment = { readonly __typename?: 'App', readonly healthCheck: string }; - -export const FooFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Foo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"App"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"healthCheck"}}]}}]} as unknown as DocumentNode; -export const AppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"App"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Foo"}}]}}]}},...FooFragmentDoc.definitions]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index 2223101c18ba..43406f48ddd5 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -1,59 +1,4 @@ { - "include": ["src/**/*.vue", "src/**/*.tsx", "cypress/**/*.ts"], - "compilerOptions": { - /* Basic Options */ - "target": "esnext", - "module": "esnext", - /* - * Allow javascript files to be compiled. - * Override this in modules that need JS - */ - "noEmit": true, - "jsx": "preserve", - "preserveWatchOutput": true, - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - /* Generates corresponding '.d.ts' file. */ - // "declaration": true, - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - /* Generates corresponding '.map' file. */ - "sourceMap": true, - /* Import emit helpers from 'tslib'. */ - "importHelpers": true, - "strictNullChecks": true, - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ - // "traceResolution": true, - "strict": true, - "noImplicitAny": false, - "noImplicitThis": false, - "forceConsistentCasingInFileNames": true, - /** - * Skip type checking of all declaration files (*.d.ts). - * TODO: Look into changing this in the future - */ - /* Additional Checks */ - "skipLibCheck": false, - /* Report errors on unused locals. */ - // "noEmit": true, - "noUnusedLocals": false, - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - /* Report error when not all code paths in function return a value. */ - "noImplicitReturns": true, - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - /* Module Resolution Options */ - "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": ["../driver/src"], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [] /* List of folders to include type definitions from. */ - "types": ["chrome", "./vue-shims", "./vite-env", "vite-plugin-icons", "@intlify/vite-plugin-vue-i18n/client", "@testing-library/cypress"], /* Type declaration files to be included in compilation. */ - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "noErrorTruncation": true, - "experimentalDecorators": true, - "resolveJsonModule": true, - "importsNotUsedAsValues": "error" - } + "extends": "../frontend-shared/tsconfig.json", + "include": ["src/**/*.vue", "src/**/*.tsx", "cypress/**/*.ts"] } diff --git a/packages/app/vue-shims.d.ts b/packages/app/vue-shims.d.ts index 71d9bf1645ed..2fc2ec079c68 100644 --- a/packages/app/vue-shims.d.ts +++ b/packages/app/vue-shims.d.ts @@ -1,14 +1,3 @@ -import { DefineComponent } from 'vue' - -declare module '*.vue' { - const component: DefineComponent - export default component -} - -declare module '*.vue.ts' { - const component: DefineComponent - export default component -} declare module 'virtual:*' { import { Component } from 'vue' diff --git a/packages/frontend-shared/cypress/support/commands.ts b/packages/frontend-shared/cypress/support/commands.ts index 6c0759c8093b..e3e0891fcb23 100644 --- a/packages/frontend-shared/cypress/support/commands.ts +++ b/packages/frontend-shared/cypress/support/commands.ts @@ -3,7 +3,7 @@ import { mount, CyMountOptions } from '@cypress/vue' import urql, { TypedDocumentNode, useQuery } from '@urql/vue' import { print, FragmentDefinitionNode } from 'graphql' import { testUrqlClient } from '@packages/frontend-shared/src/graphql/testUrqlClient' -import { Component, computed, defineComponent, h } from 'vue' +import { Component, computed, watch, defineComponent, h } from 'vue' import { ClientTestContext } from '../../src/graphql/ClientTestContext' import type { TestSourceTypeLookup } from '@packages/graphql/src/testing/testUnionType' @@ -68,6 +68,8 @@ function mountFragment { + if (result.error.value) { + cy.log('GraphQL Error', result.error.value).then(() => { + throw result.error.value + }) + } + }) + } + return { gql: computed(() => result.data.value?.[fieldName]), } }, render: (props) => { - return props.gql ? options.render(props.gql) : h('div') - }, - }), { - global: { - stubs: { - transition: false, - }, - plugins: [ - createI18n(), - { - install (app) { - app.use(urql, testUrqlClient({ - context, - rootValue: options.type(context), - })) + if (props.gql && !hasMounted) { + hasMounted = true + Cypress.log({ + displayName: 'gql', + message: (source.definitions[0] as FragmentDefinitionNode).name.value, + consoleProps () { + return { + gql: props.gql, + source: print(source), + } }, - }, - ], - }, - }).then(() => context) -} - -function mountFragmentList> (source: T[], options: MountFragmentConfig): Cypress.Chainable { - const context = new ClientTestContext({ - config: {}, - cwd: '/dev/null', - // @ts-ignore - browser: null, - global: false, - project: '/dev/null', - projectRoot: '/dev/null', - invokedFromCli: true, - testingType: 'e2e', - os: 'darwin', - _: [''], - }, {}) - - return mount(defineComponent({ - name: `mountFragmentList`, - setup () { - const getTypeCondition = (source: any) => (source.definitions[0] as any).typeCondition.name.value.toLowerCase() - const frags = source.map((src) => { - /** - * generates something like - * wizard { - * ... MyFragment - * } - * - * for each fragment passed in. - */ - const parent = getTypeCondition(src) - - return `${parent} { - ...${(src.definitions[0] as FragmentDefinitionNode).name.value} - }` - }) - - const query = ` - query MountFragmentTest { - ${frags.join('\n')} - } - - ${source.map(print)} - ` - - const result = useQuery({ - query, - }) - - return { - gql: computed(() => result.data.value), + }).end() } - }, - render: (props) => { + return props.gql ? options.render(props.gql) : h('div') }, }), { @@ -186,7 +138,9 @@ function mountFragmentList { + return mountFragment(source, options, true) +}) type GetRootType = T extends TypedDocumentNode ? U extends { __typename?: infer V } @@ -200,12 +154,14 @@ type MountFragmentConfig = { variables?: T['__variablesType'] render: (frag: Exclude) => JSX.Element type: (ctx: ClientTestContext) => GetRootType + expectError?: boolean } & CyMountOptions type MountFragmentListConfig = { variables?: T['__variablesType'] - render: (frag: Exclude) => JSX.Element + render: (frag: Exclude[]) => JSX.Element type: (ctx: ClientTestContext) => GetRootType[] + expectError?: boolean } & CyMountOptions declare global { @@ -226,7 +182,7 @@ declare global { * Mount helper for a component with a GraphQL fragment, as a list */ mountFragmentList>( - fragment: T[], + fragment: T, config: MountFragmentListConfig ): Cypress.Chainable } diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index 0614fdec6bbd..a5d015b36dd4 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -3,7 +3,8 @@ "version": "0.0.0-development", "private": true, "scripts": { - "build-prod": "tsc", + "build-prod": "tsc || echo 'built, with type errors'", + "check-ts": "tsc --noEmit", "clean-deps": "rm -rf node_modules", "clean": "rm -f src/*.js src/**/*.js" }, @@ -11,8 +12,10 @@ "devDependencies": { "@intlify/vite-plugin-vue-i18n": "2.4.0", "@testing-library/cypress": "8.0.0", - "@urql/core": "2.1.5", - "@urql/exchange-execute": "^1.0.4", + "@urql/core": "2.3.1", + "@urql/exchange-execute": "1.1.0", + "@urql/exchange-graphcache": "4.3.3", + "@urql/vue": "0.4.3", "bluebird": "3.5.3", "classnames": "2.3.1", "graphql": "^15.5.1", diff --git a/packages/frontend-shared/src/graphql/ClientTestContext.ts b/packages/frontend-shared/src/graphql/ClientTestContext.ts index 678754ea2e22..3a0cc3b73cfa 100644 --- a/packages/frontend-shared/src/graphql/ClientTestContext.ts +++ b/packages/frontend-shared/src/graphql/ClientTestContext.ts @@ -1,7 +1,11 @@ -import { BaseActions, BaseContext, DashboardProject, LocalProject } from '@packages/graphql' +import { BaseActions, BaseContext, Project } from '@packages/graphql' +import { remoteTestSchema } from '@packages/graphql/src/testing/remoteTestSchema' import type { FullConfig } from '@packages/server/lib/config' import { browsers, LaunchArgs, OpenProjectLaunchOptions } from '@packages/types' +// eslint-disable-next-line no-duplicate-imports +import * as stubData from '@packages/graphql/src/testing/remoteTestSchema' + export class ClientTestActions extends BaseActions { constructor (protected ctx: ClientTestContext) { super(ctx) @@ -76,22 +80,23 @@ export class ClientTestActions extends BaseActions { } } -const createTestProject = (projectRoot: string, ctx: BaseContext) => new LocalProject(projectRoot, ctx) +const createTestProject = (projectRoot: string, ctx: BaseContext) => new Project(projectRoot, ctx) const TEST_LAUNCH_ARGS: LaunchArgs = { config: {}, - cwd: '/dev/null', + cwd: '/current/working/dir', + _: ['/current/working/dir'], + projectRoot: '/project/root', + invokedFromCli: false, browser: browsers[0], global: false, - project: '/dev/null', - projectRoot: '/dev/null', - invokedFromCli: true, + project: '/project/root', testingType: 'e2e', - os: 'darwin', - _: [''], + os: 'linux', } export class ClientTestContext extends BaseContext { + _remoteSchema = remoteTestSchema constructor (_launchArgs?: LaunchArgs, _launchOptions?: OpenProjectLaunchOptions) { super(_launchArgs ?? TEST_LAUNCH_ARGS, _launchOptions ?? {}) } @@ -100,7 +105,8 @@ export class ClientTestContext extends BaseContext { readonly projects = [] // localProjects: Project[] = [this.testProject] - dashboardProjects: DashboardProject[] = [] - localProjects: LocalProject[] = [createTestProject('/new/project', this)] + localProjects: Project[] = [createTestProject('/new/project', this)] viewer = null + + stubData = stubData } diff --git a/packages/frontend-shared/src/graphql/testUrqlClient.ts b/packages/frontend-shared/src/graphql/testUrqlClient.ts index afc7a51ce030..47d6ac622321 100644 --- a/packages/frontend-shared/src/graphql/testUrqlClient.ts +++ b/packages/frontend-shared/src/graphql/testUrqlClient.ts @@ -1,7 +1,8 @@ -import { Client, createClient, dedupExchange, errorExchange, cacheExchange } from '@urql/core' +import { Client, createClient, dedupExchange, errorExchange } from '@urql/core' import { executeExchange } from '@urql/exchange-execute' import { graphqlSchema } from '@packages/graphql' import type { ClientTestContext } from '../../src/graphql/ClientTestContext' +import { makeCacheExchange } from './urqlClient' interface TestUrqlClientConfig { context: ClientTestContext @@ -13,13 +14,13 @@ export function testUrqlClient (config: TestUrqlClientConfig): Client { url: '/graphql', exchanges: [ dedupExchange, - cacheExchange, errorExchange({ onError (error) { // eslint-disable-next-line console.error(error) }, }), + makeCacheExchange(), executeExchange({ schema: graphqlSchema, ...config, diff --git a/packages/frontend-shared/src/graphql/urqlClient.ts b/packages/frontend-shared/src/graphql/urqlClient.ts index a82a8e2ed88a..b7e804e436c2 100644 --- a/packages/frontend-shared/src/graphql/urqlClient.ts +++ b/packages/frontend-shared/src/graphql/urqlClient.ts @@ -3,22 +3,33 @@ import { createClient, dedupExchange, errorExchange, - cacheExchange, fetchExchange, } from '@urql/core' +import { cacheExchange as graphcacheExchange } from '@urql/exchange-graphcache' + +export function makeCacheExchange () { + return graphcacheExchange({ + keys: { + App: (data) => data.__typename, + Wizard: (data) => data.__typename, + }, + }) +} export function makeUrqlClient (): Client { return createClient({ url: 'http://localhost:52159/graphql', + requestPolicy: 'cache-and-network', exchanges: [ dedupExchange, - cacheExchange, errorExchange({ onError (error) { // eslint-disable-next-line console.error(error) }, }), + // https://formidable.com/open-source/urql/docs/graphcache/errors/ + makeCacheExchange(), fetchExchange, ], }) diff --git a/packages/frontend-shared/vue-shims.d.ts b/packages/frontend-shared/vue-shims.d.ts index 71d9bf1645ed..cc5202ab6164 100644 --- a/packages/frontend-shared/vue-shims.d.ts +++ b/packages/frontend-shared/vue-shims.d.ts @@ -1,15 +1,3 @@ -import { DefineComponent } from 'vue' - -declare module '*.vue' { - const component: DefineComponent - export default component -} - -declare module '*.vue.ts' { - const component: DefineComponent - export default component -} - declare module 'virtual:*' { import { Component } from 'vue' const src: Component diff --git a/packages/graphql/.eslintrc.json b/packages/graphql/.eslintrc.json index 46ed04aa840b..0eda50e708ec 100644 --- a/packages/graphql/.eslintrc.json +++ b/packages/graphql/.eslintrc.json @@ -1,5 +1,6 @@ { "extends": [ + "plugin:@cypress/dev/general", "plugin:@cypress/dev/tests" ], "parser": "@typescript-eslint/parser", @@ -15,8 +16,45 @@ "./src/entities/**/*.ts" ], "rules": { + "no-useless-constructor": "off", "@typescript-eslint/explicit-function-return-type": [ "error" + ], + "no-restricted-imports": [ + "error", + "assert", + "buffer", + "child_process", + "cluster", + "crypto", + "dgram", + "dns", + "domain", + "events", + "freelist", + "fs", + "http", + "https", + "module", + "net", + "os", + "path", + "punycode", + "querystring", + "readline", + "repl", + "smalloc", + "stream", + "string_decoder", + "sys", + "timers", + "tls", + "tracing", + "tty", + "url", + "util", + "vm", + "zlib" ] } } diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 449ed03cf519..473cac660414 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -5,8 +5,8 @@ "main": "index.js", "browser": "src/index.ts", "scripts": { - "types": "tsc --noEmit", - "build-prod": "yarn gulp nexusCodegen && tsc", + "build-prod": "tsc || echo 'built, with errors'", + "check-ts": "tsc --noEmit", "clean-deps": "rm -rf node_modules", "clean": "rm -f ./src/*.js ./src/**/*.js ./src/**/**/*.js ./test/**/*.js || echo 'cleaned'", "postinstall": "echo '@packages/graphql needs: yarn build'", @@ -14,14 +14,22 @@ "test-integration": "mocha -r @packages/ts/register test/integration/**/*.spec.ts --config ./test/.mocharc.js --exit" }, "dependencies": { + "@graphql-tools/batch-delegate": "^8.0.12", + "@graphql-tools/delegate": "^8.1.1", + "@graphql-tools/utils": "^8.1.2", + "@graphql-tools/wrap": "^8.0.13", "cors": "2.8.5", + "cross-fetch": "^3.1.4", "dedent": "^0.7.0", "express": "4.17.1", "express-graphql": "^0.12.0", + "fake-uuid": "^1.0.0", + "getenv": "^1.0.0", "graphql": "^15.5.1", + "graphql-relay": "^0.9.0", "graphql-scalars": "^1.10.0", - "nexus": "^1.1.0", - "nexus-decorators": "0.2.3" + "nexus": "^1.2.0-next.15", + "nexus-decorators": "^0.2.5" }, "devDependencies": { "@packages/types": "0.0.0-development", @@ -32,7 +40,7 @@ }, "files": [ "src", - "schema.graphql" + "schemas" ], "types": "src/index.ts" } diff --git a/packages/graphql/schemas/cloud.graphql b/packages/graphql/schemas/cloud.graphql new file mode 100644 index 000000000000..fbe5a7e3f600 --- /dev/null +++ b/packages/graphql/schemas/cloud.graphql @@ -0,0 +1,381 @@ +### This file was generated by Nexus Schema +### Do not make changes to this file directly + +""" +A CloudOrganization represents an Organization stored in the Cypress Cloud +""" +type CloudOrganization implements Node { + """ + Globally unique identifier representing a concrete GraphQL ObjectType + """ + id: ID! + + """ + Name of the organization + """ + name: String + + """ + A connection for cloud projects associated with this organization + """ + projects( + after: String + before: String + first: Int + last: Int + ): CloudProjectConnection +} + +""" +A Connection adhering to the Relay Specification +""" +type CloudOrganizationConnection { + """ + A list of edges. + """ + edges: [CloudOrganizationEdge!]! + + """ + A list of nodes. + """ + nodes: [CloudOrganization!]! + + """ + PageInfo result for the connection + """ + pageInfo: PageInfo! +} + +type CloudOrganizationEdge { + cursor: String! + + """ + An edge adhering to the Relay Connection spec + """ + node: CloudOrganization! +} + +""" +A CloudProject represents a Project stored in the Cypress Cloud +""" +type CloudProject implements Node { + """ + Globally unique identifier representing a concrete GraphQL ObjectType + """ + id: ID! + + """ + The latest run for a given spec + """ + latestRun: CloudRun + + """ + The organization the project is a member of + """ + organization: CloudOrganization + + """ + Record keys for the service + """ + recordKeys: [CloudRecordKey!] + + """ + A connection field type + """ + runs( + after: String + before: String + cypressVersion: String + first: Int + last: Int + status: CloudRunStatus + ): CloudRunConnection + + """ + Unique identifier for a Project + """ + slug: String! +} + +""" +A Connection adhering to the Relay Specification +""" +type CloudProjectConnection { + """ + A list of edges. + """ + edges: [CloudProjectEdge!]! + + """ + A list of nodes. + """ + nodes: [CloudProject!]! + + """ + PageInfo result for the connection + """ + pageInfo: PageInfo! +} + +type CloudProjectEdge { + cursor: String! + + """ + An edge adhering to the Relay Connection spec + """ + node: CloudProject! +} + +type CloudRecordKey implements Node { + createdAt: DateTime + + """ + Globally unique identifier representing a concrete GraphQL ObjectType + """ + id: ID! + + """ + The Record Key + """ + key: String + lastUsedAt: DateTime +} + +""" +A Recorded run of the Test Runner, typically to the cloud +""" +type CloudRun implements Node { + commitInfo: CloudRunCommitInfo + createdAt: Date + + """ + Globally unique identifier representing a concrete GraphQL ObjectType + """ + id: ID! + status: CloudRunStatus + + """ + Total duration of the run in milliseconds, accounting for any parallelization + """ + totalDuration: Int + + """ + This is the number of failed tests across all groups in the run + """ + totalFailed: Int + + """ + This is the number of passed tests across all groups in the run + """ + totalPassed: Int + + """ + This is the number of pending tests across all groups in the run + """ + totalPending: Int + + """ + This is the number of running tests across all groups in the run + """ + totalRunning: Int + + """ + This is the number of skipped tests across all groups in the run + """ + totalSkipped: Int + + """ + This is the number of tests across all groups in the run + """ + totalTests: Int +} + +type CloudRunCommitInfo { + authorAvatar: String + authorEmail: String + authorName: String + branch: String + branchUrl: String + message( + """ + Number of characters to truncate the commit message to + """ + truncate: Int + ): String + sha: String + summary: String + url: String +} + +""" +Connection type for CloudRun, adhering to the Relay Connection spec +""" +type CloudRunConnection { + """ + A list of edges. + """ + edges: [CloudRunEdge!]! + + """ + A list of nodes. + """ + nodes: [CloudRun!]! + + """ + PageInfo result for the connection + """ + pageInfo: PageInfo! +} + +""" +Represents an individual Cloud test Run +""" +type CloudRunEdge { + cursor: String! + + """ + The item at the end of the edge. + """ + node: CloudRun! +} + +""" +Possible check status of the test run +""" +enum CloudRunStatus { + CANCELLED + ERRORED + FAILED + NOTESTS + OVERLIMIT + PASSED + RUNNING + TIMEDOUT +} + +""" +A CloudUser represents an User stored in the Cypress Cloud +""" +type CloudUser implements Node { + email: String + + """ + The display name of the user, if we have one + """ + fullName: String + + """ + Globally unique identifier representing a concrete GraphQL ObjectType + """ + id: ID! + + """ + A connection field type + """ + organizations( + after: String + before: String + first: Int + last: Int + ): CloudOrganizationConnection + + """ + Whether this user is the currently authenticated user + """ + userIsViewer: Boolean! +} + +""" +A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. +""" +scalar Date + +""" +A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. +""" +scalar DateTime + +""" +Mutations for the Cypress Cloud +""" +type Mutation { + """ + Adding as a test + """ + test: Boolean +} + +""" +Implements the Relay Node spec +""" +interface Node { + """ + Globally unique identifier representing a concrete GraphQL ObjectType + """ + id: ID! +} + +""" +PageInfo object, adhering to the Relay Connection Spec: + +https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo +""" +type PageInfo { + """ + Must be the cursor corresponding to the last node in edges. Null if no such node exists + """ + endCursor: String + + """ + Used to indicate whether more edges exist following the set defined by the clients arguments. + If the client is paginating with first/after, then the server must return true if further edges + exist, otherwise false. If the client is paginating with last/before, then the client may return + true if edges further from before exist, if it can do so efficiently, otherwise may return false. + """ + hasNextPage: Boolean! + + """ + Used to indicate whether more edges exist prior to the set defined by the clients arguments. + If the client is paginating with last/before, then the server must return true if prior edges exist, + otherwise false. If the client is paginating with first/after, then the client may return true if + edges prior to after exist, if it can do so efficiently, otherwise may return false. + """ + hasPreviousPage: Boolean! + + """ + Must be the cursor corresponding to the first node in edges. Null if no such node exists + """ + startCursor: String +} + +type Query { + """ + Returns an object conforming to the Relay spec + """ + cloudNode( + """ + An ID for a Node conforming to the Relay spec + """ + id: ID! + ): Node + + """ + Lookup an individual project by the slug + """ + cloudProjectBySlug(slug: String!): CloudProject + + """ + Lookup a list of projects by their slug + """ + cloudProjectsBySlugs( + """ + A list of Project slugs + """ + slugs: [String!]! + ): [CloudProject] + + """ + A user within the Cypress Cloud + """ + cloudViewer: CloudUser +} diff --git a/packages/graphql/schema.graphql b/packages/graphql/schemas/schema.graphql similarity index 56% rename from packages/graphql/schema.graphql rename to packages/graphql/schemas/schema.graphql index 3de1e3b9aaeb..ebadc0067ee9 100644 --- a/packages/graphql/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -5,7 +5,7 @@ """Namespace for information related to the app""" type App { """Active project""" - activeProject: LocalProject + activeProject: Project """Browsers found that are compatible with Cypress""" browsers: [Browser!]! @@ -20,14 +20,16 @@ type App { isInGlobalMode: Boolean! """All known projects for the app""" - projects: [LocalProject!]! + projects: [Project!]! } """Container representing a browser""" type Browser { channel: String! + disabled: Boolean! displayName: String! family: BrowserFamily! + id: String! majorVersion: String name: String! path: String! @@ -39,18 +41,192 @@ enum BrowserFamily { firefox } -"""A Cypress project is a container""" -type DashboardProject { +""" +A CloudOrganization represents an Organization stored in the Cypress Cloud +""" +type CloudOrganization implements Node { + """Globally unique identifier representing a concrete GraphQL ObjectType""" id: ID! - """Used to associate project with Cypress cloud""" - projectId: String - projectRoot: String! - recordKeys: [String!] - runs: [RunGroup!] - title: String! + """Name of the organization""" + name: String + + """A connection for cloud projects associated with this organization""" + projects(after: String, before: String, first: Int, last: Int): CloudProjectConnection +} + +"""A Connection adhering to the Relay Specification""" +type CloudOrganizationConnection { + """A list of edges.""" + edges: [CloudOrganizationEdge!]! + + """A list of nodes.""" + nodes: [CloudOrganization!]! + + """PageInfo result for the connection""" + pageInfo: PageInfo! +} + +type CloudOrganizationEdge { + cursor: String! + + """An edge adhering to the Relay Connection spec""" + node: CloudOrganization! +} + +"""A CloudProject represents a Project stored in the Cypress Cloud""" +type CloudProject implements Node { + """Globally unique identifier representing a concrete GraphQL ObjectType""" + id: ID! + + """The latest run for a given spec""" + latestRun: CloudRun + + """The organization the project is a member of""" + organization: CloudOrganization + + """Record keys for the service""" + recordKeys: [CloudRecordKey!] + + """A connection field type""" + runs(after: String, before: String, cypressVersion: String, first: Int, last: Int, status: CloudRunStatus): CloudRunConnection + + """Unique identifier for a Project""" + slug: String! +} + +"""A Connection adhering to the Relay Specification""" +type CloudProjectConnection { + """A list of edges.""" + edges: [CloudProjectEdge!]! + + """A list of nodes.""" + nodes: [CloudProject!]! + + """PageInfo result for the connection""" + pageInfo: PageInfo! +} + +type CloudProjectEdge { + cursor: String! + + """An edge adhering to the Relay Connection spec""" + node: CloudProject! +} + +type CloudRecordKey implements Node { + createdAt: DateTime + + """Globally unique identifier representing a concrete GraphQL ObjectType""" + id: ID! + + """The Record Key""" + key: String + lastUsedAt: DateTime +} + +"""A Recorded run of the Test Runner, typically to the cloud""" +type CloudRun implements Node { + commitInfo: CloudRunCommitInfo + createdAt: Date + + """Globally unique identifier representing a concrete GraphQL ObjectType""" + id: ID! + status: CloudRunStatus + + """ + Total duration of the run in milliseconds, accounting for any parallelization + """ + totalDuration: Int + + """This is the number of failed tests across all groups in the run""" + totalFailed: Int + + """This is the number of passed tests across all groups in the run""" + totalPassed: Int + + """This is the number of pending tests across all groups in the run""" + totalPending: Int + + """This is the number of running tests across all groups in the run""" + totalRunning: Int + + """This is the number of skipped tests across all groups in the run""" + totalSkipped: Int + + """This is the number of tests across all groups in the run""" + totalTests: Int +} + +type CloudRunCommitInfo { + authorAvatar: String + authorEmail: String + authorName: String + branch: String + branchUrl: String + message( + """Number of characters to truncate the commit message to""" + truncate: Int + ): String + sha: String + summary: String + url: String +} + +"""Connection type for CloudRun, adhering to the Relay Connection spec""" +type CloudRunConnection { + """A list of edges.""" + edges: [CloudRunEdge!]! + + """A list of nodes.""" + nodes: [CloudRun!]! + + """PageInfo result for the connection""" + pageInfo: PageInfo! +} + +"""Represents an individual Cloud test Run""" +type CloudRunEdge { + cursor: String! + + """The item at the end of the edge.""" + node: CloudRun! +} + +"""Possible check status of the test run""" +enum CloudRunStatus { + CANCELLED + ERRORED + FAILED + NOTESTS + OVERLIMIT + PASSED + RUNNING + TIMEDOUT } +"""A CloudUser represents an User stored in the Cypress Cloud""" +type CloudUser implements Node { + email: String + + """The display name of the user, if we have one""" + fullName: String + + """Globally unique identifier representing a concrete GraphQL ObjectType""" + id: ID! + + """A connection field type""" + organizations(after: String, before: String, first: Int, last: Int): CloudOrganizationConnection + + """Whether this user is the currently authenticated user""" + userIsViewer: Boolean! +} + +""" +A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. +""" +scalar Date + """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ @@ -70,23 +246,6 @@ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http:// """ scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") -"""A Cypress project is a container""" -type LocalProject { - id: ID! - - """Whether the user configured this project to use Component Testing""" - isFirstTimeCT: Boolean! - - """Whether the user configured this project to use e2e Testing""" - isFirstTimeE2E: Boolean! - - """Used to associate project with Cypress cloud""" - projectId: String - projectRoot: String! - resolvedConfig: ResolvedConfig - title: String! -} - type Mutation { """Create a Cypress config file for a new project""" appCreateConfigFile(code: String!, configFilename: String!): App @@ -100,10 +259,10 @@ type Mutation { launchOpenProject: App """Auth with Cypress Cloud""" - login: Viewer + login: Query """Log out of Cypress Cloud""" - logout: Viewer + logout: Query """Set the current navigation item""" navigationMenuSetItem(type: NavItem!): NavigationMenu @@ -114,9 +273,6 @@ type Mutation { """Navigates backward in the wizard""" wizardNavigate(direction: WizardNavigateDirection!): Wizard - """Navigates forward in the wizard""" - wizardNavigateForward: Wizard - """Sets the frontend bundler we want to use for the project""" wizardSetBundler(bundler: SupportedBundlers!): Wizard @@ -150,10 +306,49 @@ type NavigationItem { """Container for state associated with the side navigation menu""" type NavigationMenu { - items: [NavigationItem]! + items: [NavigationItem!]! selected: NavItem! } +"""Implements the Relay Node spec""" +interface Node { + """Globally unique identifier representing a concrete GraphQL ObjectType""" + id: ID! +} + +""" +PageInfo object, adhering to the Relay Connection Spec: + +https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo +""" +type PageInfo { + """ + Must be the cursor corresponding to the last node in edges. Null if no such node exists + """ + endCursor: String + + """ + Used to indicate whether more edges exist following the set defined by the clients arguments. + If the client is paginating with first/after, then the server must return true if further edges + exist, otherwise false. If the client is paginating with last/before, then the client may return + true if edges further from before exist, if it can do so efficiently, otherwise may return false. + """ + hasNextPage: Boolean! + + """ + Used to indicate whether more edges exist prior to the set defined by the clients arguments. + If the client is paginating with last/before, then the server must return true if prior edges exist, + otherwise false. If the client is paginating with first/after, then the client may return true if + edges prior to after exist, if it can do so efficiently, otherwise may return false. + """ + hasPreviousPage: Boolean! + + """ + Must be the cursor corresponding to the first node in edges. Null if no such node exists + """ + startCursor: String +} + enum PluginsState { error initialized @@ -163,11 +358,19 @@ enum PluginsState { """A Cypress project is a container""" type Project { + cloudProject: CloudProject id: ID! + """Whether the user configured this project to use Component Testing""" + isFirstTimeCT: Boolean! + + """Whether the user configured this project to use e2e Testing""" + isFirstTimeE2E: Boolean! + """Used to associate project with Cypress cloud""" projectId: String projectRoot: String! + resolvedConfig: ResolvedConfig title: String! } @@ -175,19 +378,32 @@ type Project { type Query { app: App! + """Returns an object conforming to the Relay spec""" + cloudNode( + """An ID for a Node conforming to the Relay spec""" + id: ID! + ): Node + + """Lookup an individual project by the slug""" + cloudProjectBySlug(slug: String!): CloudProject + + """Lookup a list of projects by their slug""" + cloudProjectsBySlugs( + """A list of Project slugs""" + slugs: [String!]! + ): [CloudProject] + + """A user within the Cypress Cloud""" + cloudViewer: CloudUser + """Metadata about the nagivation menu""" navigationMenu: NavigationMenu - """ - Namespace for data accessible from Cypress Cloud for authenticated users - """ - viewer: Viewer - """Metadata about the wizard, null if we arent showing the wizard""" wizard: Wizard! } -type ResolvedBooleanOption { +type ResolvedBooleanOption implements ResolvedOptionBase { from: ResolvedConfigOption type: ResolvedType! value: Boolean @@ -258,29 +474,30 @@ enum ResolvedConfigOption { """ An JSON object represented as a string via JSON.stringify. Useful for representing complex types like `env` """ -type ResolvedJsonOption { +type ResolvedJsonOption implements ResolvedOptionBase { from: ResolvedConfigOption type: ResolvedType! value: String } -type ResolvedNumberOption { +type ResolvedNumberOption implements ResolvedOptionBase { from: ResolvedConfigOption type: ResolvedType! value: String } -type ResolvedOptionBase { +interface ResolvedOptionBase { from: ResolvedConfigOption + type: ResolvedType! } -type ResolvedStringListOption { +type ResolvedStringListOption implements ResolvedOptionBase { from: ResolvedConfigOption type: ResolvedType! value: [String] } -type ResolvedStringOption { +type ResolvedStringOption implements ResolvedOptionBase { from: ResolvedConfigOption type: ResolvedType! value: String @@ -294,40 +511,6 @@ enum ResolvedType { string } -"""Represents a commit on run on Cypress Cloud""" -type RunCommit { - authorEmail: String! - authorName: String! - branch: String! - message: String! - sha: String! - url: String! -} - -"""Represents a run on Cypress Cloud""" -type RunGroup { - commit: RunCommit! - completedAt: String! - createdAt: String! - status: RunGroupStatus! - totalDuration: Int - totalFailed: Int - totalPassed: Int - totalPending: Int - totalSkipped: Int -} - -enum RunGroupStatus { - cancelled - errored - failed - noTests - passed - running - timedOut - unclaimed -} - """The bundlers that we can use with Cypress""" enum SupportedBundlers { vite @@ -345,19 +528,6 @@ type TestingTypeInfo { title: String! } -"""Namespace for information related to the viewer""" -type Viewer { - authToken: String! - email: String! - - """Active project""" - getProjectByProjectId(projectId: String!): DashboardProject - name: String! - - """All known projects for the app""" - projects: [DashboardProject] -} - """ The Wizard is a container for any state associated with initial onboarding to Cypress """ diff --git a/packages/graphql/script/codegen-mount-ts.ts b/packages/graphql/script/codegen-mount-ts.ts index 334e073f6586..9e33e509bbd9 100644 --- a/packages/graphql/script/codegen-mount-ts.ts +++ b/packages/graphql/script/codegen-mount-ts.ts @@ -11,7 +11,12 @@ const plugin: CodegenPlugin = { for (const [typeName, type] of Object.entries(typesMap)) { if (!typeName.startsWith('__') && isObjectType(type) || isInterfaceType(type)) { - typeMap.push(` ${typeName}: NexusGenObjects['${typeName}'],`) + if (isObjectType(type)) { + typeMap.push(` ${typeName}: NexusGenObjects['${typeName}'],`) + } else if (isInterfaceType(type)) { + typeMap.push(` ${typeName}: NexusGenInterfaces['${typeName}'],`) + } + if (isObjectType(type)) { objects.push(typeName) } @@ -20,7 +25,7 @@ const plugin: CodegenPlugin = { return [ `// Generated by ${path.basename(__filename)}, do not edit directly`, - `import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'`, + `import type { NexusGenObjects, NexusGenInterfaces } from '@packages/graphql/src/gen/nxs.gen'`, `export interface TestSourceTypeLookup {`, typeMap.join('\n'), `}`, diff --git a/packages/graphql/script/codegen-type-map-ts.ts b/packages/graphql/script/codegen-type-map-ts.ts new file mode 100644 index 000000000000..f8b58bbdfecd --- /dev/null +++ b/packages/graphql/script/codegen-type-map-ts.ts @@ -0,0 +1,36 @@ +import type { CodegenPlugin } from '@graphql-codegen/plugin-helpers' +import { isInterfaceType, isObjectType } from 'graphql' + +const plugin: CodegenPlugin = { + plugin: (schema, documents, config, info) => { + const typesMap = schema.getTypeMap() + + let typeMap: string[] = [] + let resolveTypeMap: string[] = [] + + for (const [typeName, type] of Object.entries(typesMap)) { + if (!typeName.startsWith('__') && isObjectType(type) || isInterfaceType(type)) { + typeMap.push(` ${typeName}: ${typeName},`) + resolveTypeMap.push(` ${typeName}: MaybeResolver<${typeName}>,`) + } + } + + return [ + `import type { GraphQLResolveInfo } from 'graphql'`, + `import type { NxsCtx } from 'nexus-decorators'`, + 'export type MaybeResolver = {', + ` [K in keyof T]: K extends 'id' | '__typename' ? T[K] : T[K] | ((args: any, ctx: NxsCtx, info: GraphQLResolveInfo) => MaybeResolver)`, + '}', + '', + `export interface CodegenTypeMap {`, + typeMap.join('\n'), + `}`, + '', + `export interface CodegenResolveTypeMap {`, + resolveTypeMap.join('\n'), + '}', + ].join('\n') + }, +} + +export default plugin diff --git a/packages/graphql/script/codegen-type-map.js b/packages/graphql/script/codegen-type-map.js new file mode 100644 index 000000000000..fe184cfede67 --- /dev/null +++ b/packages/graphql/script/codegen-type-map.js @@ -0,0 +1,2 @@ +require('@packages/ts/register') +module.exports = require('./codegen-type-map-ts').default diff --git a/packages/graphql/src/actions/BaseActions.ts b/packages/graphql/src/actions/BaseActions.ts index b3e154c6ea69..6c6cfaff8e42 100644 --- a/packages/graphql/src/actions/BaseActions.ts +++ b/packages/graphql/src/actions/BaseActions.ts @@ -1,8 +1,7 @@ import type { BaseContext } from '../context/BaseContext' -import type { RunGroup } from '../entities/run' -import type { FoundBrowser, OpenProjectLaunchOptions, FullConfig, LaunchOpts, LaunchArgs } from '@packages/types' -import type { LocalProject } from '../entities' +import type { FoundBrowser, OpenProjectLaunchOptions, LaunchOpts, LaunchArgs, FullConfig } from '@packages/types' import type { BrowserContract } from '../contracts/BrowserContract' +import type { Project } from '../entities/Project' /** * Acts as the contract for all actions, inherited by: @@ -19,14 +18,12 @@ export abstract class BaseActions { abstract createConfigFile (code: string, configFilename: string): void - abstract addProject (projectRoot: string): LocalProject + abstract addProject (projectRoot: string): Project abstract getProjectId (projectRoot: string): Promise abstract authenticate (): Promise abstract logout (): Promise - abstract getRuns (payload: { projectId: string, authToken: string }): Promise - abstract getRecordKeys (payload: { projectId: string, authToken: string }): Promise abstract getBrowsers (): Promise abstract initializeOpenProject (args: LaunchArgs, options: OpenProjectLaunchOptions, browsers: any): Promise diff --git a/packages/graphql/src/constants/index.ts b/packages/graphql/src/constants/index.ts index a945cc9bbc8c..10087ee1b039 100644 --- a/packages/graphql/src/constants/index.ts +++ b/packages/graphql/src/constants/index.ts @@ -3,5 +3,4 @@ export * from './browserConstants' export * from './projectConstants' -export * from './runConstants' export * from './wizardConstants' diff --git a/packages/graphql/src/constants/runConstants.ts b/packages/graphql/src/constants/runConstants.ts deleted file mode 100644 index 5b4019d2541c..000000000000 --- a/packages/graphql/src/constants/runConstants.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { enumType } from 'nexus' - -// https://github.com/cypress-io/cypress-services/blob/e4689bd41e84954964653a7022f09e7aae962f46/packages/common/src/enums.ts -// need to find a way to share code between services and runner monorepo - -export const RUN_GROUP_STATUS = [ - 'unclaimed', - 'running', - 'errored', - 'failed', - 'timedOut', - 'passed', - 'noTests', - 'cancelled', -] as const - -export type RunGroupStatus = typeof RUN_GROUP_STATUS[number] - -export const RunGroupStatusEnum = enumType({ - name: 'RunGroupStatus', - members: RUN_GROUP_STATUS, -}) diff --git a/packages/graphql/src/context/BaseContext.ts b/packages/graphql/src/context/BaseContext.ts index 48ac83c203a7..0a45dca62472 100644 --- a/packages/graphql/src/context/BaseContext.ts +++ b/packages/graphql/src/context/BaseContext.ts @@ -1,6 +1,37 @@ +import { delegateToSchema } from '@graphql-tools/delegate' +import { batchDelegateToSchema } from '@graphql-tools/batch-delegate' import type { LaunchArgs, OpenProjectLaunchOptions } from '@packages/types' +import type { GraphQLResolveInfo, GraphQLSchema } from 'graphql' import type { BaseActions } from '../actions/BaseActions' -import { App, Wizard, NavigationMenu, LocalProject, Viewer, DashboardProject } from '../entities' +import { App, Wizard, NavigationMenu, Project } from '../entities' +import type { NexusGenObjects } from '../gen/nxs.gen' +import type { Query as CloudQuery } from '../gen/cloud-source-types.gen' +import type { NxsQueryResult } from 'nexus-decorators' + +export interface AuthenticatedUser { + name?: string + email?: string + authToken?: string +} + +type PotentialFields = Exclude + +interface FieldArgMapping { + cloudProjectsBySlugs: string +} + +type KnownBatchFields = PotentialFields & keyof FieldArgMapping + +const FieldConfig: Record = { + cloudProjectsBySlugs: 'slugs', +} + +export interface DelegateToRemoteQueryBatchedConfig { + fieldName: F + info: GraphQLResolveInfo + rootValue?: object + key?: FieldArgMapping[F] +} /** * The "Base Context" is the class type that we will use to encapsulate the server state. @@ -10,16 +41,69 @@ import { App, Wizard, NavigationMenu, LocalProject, Viewer, DashboardProject } f * without the need to endlessly mock things. */ export abstract class BaseContext { + protected _authenticatedUser: AuthenticatedUser | null = null + protected abstract _remoteSchema: GraphQLSchema + + get authenticatedUser () { + return this._authenticatedUser ?? null + } + + setAuthenticatedUser (authUser: AuthenticatedUser | null) { + this._authenticatedUser = authUser + + return this + } + abstract readonly actions: BaseActions - abstract localProjects: LocalProject[] - abstract dashboardProjects: DashboardProject[] - abstract viewer: null | Viewer + abstract localProjects: Project[] constructor (private _launchArgs: LaunchArgs, private _launchOptions: OpenProjectLaunchOptions) {} + app = new App(this) wizard = new Wizard(this) navigationMenu = new NavigationMenu() - app = new App(this) + + cloudProjectsBySlug (slug: string, info: GraphQLResolveInfo) { + return this.delegateToRemoteQueryBatched({ + info, + key: slug, + fieldName: 'cloudProjectsBySlugs', + }) + } + + delegateToRemoteQueryBatched (config: DelegateToRemoteQueryBatchedConfig): NxsQueryResult | null { + try { + return batchDelegateToSchema({ + schema: this._remoteSchema, + info: config.info, + context: this, + rootValue: config.rootValue ?? {}, + operation: 'query', + fieldName: config.fieldName, + key: config.key, + argsFromKeys: (keys) => ({ [FieldConfig[config.fieldName]]: keys }), + }) + } catch (e) { + this.logError(e) + + return null + } + } + + async delegateToRemoteQuery (info: GraphQLResolveInfo, rootValue = {}): Promise { + try { + return delegateToSchema({ + schema: this._remoteSchema, + info, + context: this, + rootValue, + }) + } catch (e) { + this.logError(e) + + return null + } + } get activeProject () { return this.app.activeProject @@ -33,5 +117,11 @@ export abstract class BaseContext { return this._launchOptions } + logError (e: unknown) { + // TODO(tim): handle this consistently + // eslint-disable-next-line no-console + console.error(e) + } + isFirstOpen = false } diff --git a/packages/graphql/src/entities/App.ts b/packages/graphql/src/entities/App.ts index 3f42b1afa9ce..3c9373fbeca8 100644 --- a/packages/graphql/src/entities/App.ts +++ b/packages/graphql/src/entities/App.ts @@ -1,6 +1,6 @@ import { nxs, NxsResult } from 'nexus-decorators' import type { BaseContext } from '../context/BaseContext' -import { LocalProject } from './LocalProject' +import { Project } from './Project' import { Browser } from './Browser' import type { FoundBrowser } from '@packages/types' @@ -36,7 +36,7 @@ export class App { return hasGlobalModeArg || isMissingActiveProject } - @nxs.field.type(() => LocalProject, { + @nxs.field.type(() => Project, { description: 'Active project', }) get activeProject (): NxsResult<'App', 'activeProject'> { @@ -44,7 +44,7 @@ export class App { return this.ctx.localProjects[0]! } - @nxs.field.nonNull.list.nonNull.type(() => LocalProject, { + @nxs.field.nonNull.list.nonNull.type(() => Project, { description: 'All known projects for the app', }) get projects (): NxsResult<'App', 'projects'> { diff --git a/packages/graphql/src/entities/Browser.ts b/packages/graphql/src/entities/Browser.ts index 58e1471c6939..0288c77902bf 100644 --- a/packages/graphql/src/entities/Browser.ts +++ b/packages/graphql/src/entities/Browser.ts @@ -8,6 +8,11 @@ import type { BrowserContract } from '../contracts/BrowserContract' export class Browser implements BrowserContract { constructor (private _config: BrowserContract) {} + @nxs.field.nonNull.string() + get id (): NxsResult<'Browser', 'id'> { + return `${this.config.name}-${this.config.version}-${this.config.displayName}` + } + @nxs.field.nonNull.string() get name (): NxsResult<'Browser', 'name'> { return this._config.name @@ -43,6 +48,11 @@ export class Browser implements BrowserContract { return this._config.majorVersion?.toString() ?? null } + @nxs.field.nonNull.boolean() + get disabled (): NxsResult<'Browser', 'disabled'> { + return false + } + get config (): BrowserContract { return this._config } diff --git a/packages/graphql/src/entities/DashboardProject.ts b/packages/graphql/src/entities/DashboardProject.ts deleted file mode 100644 index 9785a0630491..000000000000 --- a/packages/graphql/src/entities/DashboardProject.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { nxs, NxsResult } from 'nexus-decorators' -import type { BaseContext } from '../context/BaseContext' -import { Project } from './Project' -import { RunGroup } from './run' - -@nxs.objectType({ - description: 'A Cypress project is a container', -}) -export class DashboardProject extends Project { - constructor ( - projectRoot: string, - private context: BaseContext, - private authToken: string, - ) { - super(projectRoot, context) - } - - @nxs.field.list.nonNull.type(() => RunGroup) - async runs (): Promise> { - const projectId = await this.projectId() - - if (!projectId) { - throw Error('projectId required to fetch runs') - } - - const result = await this.context.actions.getRuns({ - projectId, - authToken: this.authToken, - }) - - return result - } - - @nxs.field.list.nonNull.string() - async recordKeys (): Promise> { - const projectId = await this.projectId() - - if (!projectId) { - throw Error('projectId required to fetch runs') - } - - const result = await this.context.actions.getRecordKeys({ - projectId, - authToken: this.authToken, - }) - - return result - } -} diff --git a/packages/graphql/src/entities/LocalProject.ts b/packages/graphql/src/entities/LocalProject.ts deleted file mode 100644 index d7e6854cc319..000000000000 --- a/packages/graphql/src/entities/LocalProject.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { nxs, NxsResult } from 'nexus-decorators' -import { Project } from './Project' -import { ResolvedConfig } from './ResolvedConfig' - -@nxs.objectType({ - description: 'A Cypress project is a container', -}) -export class LocalProject extends Project { - _ctPluginsInitialized: boolean = false - _e2ePluginsInitialized: boolean = false - - @nxs.field.nonNull.boolean({ - description: 'Whether the user configured this project to use Component Testing', - }) - get isFirstTimeCT (): NxsResult<'LocalProject', 'isFirstTimeCT'> { - return this.ctx.actions.isFirstTime(this.projectRoot, 'component') - } - - @nxs.field.nonNull.boolean({ - description: 'Whether the user configured this project to use e2e Testing', - }) - get isFirstTimeE2E (): NxsResult<'LocalProject', 'isFirstTimeE2E'> { - return this.ctx.actions.isFirstTime(this.projectRoot, 'e2e') - } - - @nxs.field.type(() => ResolvedConfig) - resolvedConfig (): NxsResult<'LocalProject', 'resolvedConfig'> { - const cfg = this.ctx.actions.resolveOpenProjectConfig() - - if (!cfg) { - throw Error('openProject.getConfig is null. Have you initialized the current project?') - } - - return new ResolvedConfig(cfg.resolved) - } - - setE2EPluginsInitialized (init: boolean): void { - this._e2ePluginsInitialized = init - } - - get e2ePluginsInitialized (): boolean { - return this._e2ePluginsInitialized - } - - setCtPluginsInitialized (init: boolean): void { - this._ctPluginsInitialized = init - } - - get ctPluginsInitialized (): boolean { - return this._ctPluginsInitialized - } -} diff --git a/packages/graphql/src/entities/Mutation.ts b/packages/graphql/src/entities/Mutation.ts index 00a33121291b..a57caca8e262 100644 --- a/packages/graphql/src/entities/Mutation.ts +++ b/packages/graphql/src/entities/Mutation.ts @@ -1,6 +1,7 @@ import Debug from 'debug' import { mutationType, nonNull } from 'nexus' import { BundlerEnum, FrontendFrameworkEnum, NavItemEnum, TestingTypeEnum, WizardNavigateDirectionEnum } from '../constants' +import { Query } from './Query' const debug = Debug('cypress:graphql:mutation') @@ -43,12 +44,6 @@ export const mutation = mutationType({ resolve: (root, args, ctx) => ctx.wizard.setManualInstall(args.isManual), }) - t.field('wizardNavigateForward', { - type: 'Wizard', - description: 'Navigates forward in the wizard', - resolve: (_, __, ctx) => ctx.wizard.navigate('forward'), - }) - t.field('wizardNavigate', { type: 'Wizard', args: { @@ -98,27 +93,27 @@ export const mutation = mutationType({ }) t.field('login', { - type: 'Viewer', + type: 'Query', description: 'Auth with Cypress Cloud', async resolve (_root, args, ctx) { // already authenticated this session - just return - if (ctx.viewer) { - return ctx.viewer + if (ctx.authenticatedUser) { + return new Query() } await ctx.actions.authenticate() - return ctx.viewer + return new Query() }, }) t.field('logout', { - type: 'Viewer', + type: 'Query', description: 'Log out of Cypress Cloud', async resolve (_root, args, ctx) { await ctx.actions.logout() - return ctx.viewer + return new Query() }, }) diff --git a/packages/graphql/src/entities/NavigationMenu.ts b/packages/graphql/src/entities/NavigationMenu.ts index 9147a8f40efd..4d348c2a19d7 100644 --- a/packages/graphql/src/entities/NavigationMenu.ts +++ b/packages/graphql/src/entities/NavigationMenu.ts @@ -8,7 +8,7 @@ import { NavigationItem } from './NavigationItem' export class NavigationMenu { private _selected: NavItem = 'projectSetup' - @nxs.field.nonNull.list.type(() => NavigationItem) + @nxs.field.nonNull.list.nonNull.type(() => NavigationItem) get items (): NxsResult<'NavigationMenu', 'items'> { return NAV_ITEM.map((item) => new NavigationItem(this, item)) } diff --git a/packages/graphql/src/entities/Project.ts b/packages/graphql/src/entities/Project.ts index 3d0dd9dbb53d..78ad7ec15db2 100644 --- a/packages/graphql/src/entities/Project.ts +++ b/packages/graphql/src/entities/Project.ts @@ -1,32 +1,97 @@ -import { nxs, NxsResult } from 'nexus-decorators' +import type { GraphQLResolveInfo } from 'graphql' +import { nxs, NxsCtx, NxsResult } from 'nexus-decorators' import type { BaseContext } from '../context/BaseContext' import type { ProjectContract } from '../contracts/ProjectContract' +import { ResolvedConfig } from './ResolvedConfig' @nxs.objectType({ description: 'A Cypress project is a container', }) export class Project implements ProjectContract { + _ctPluginsInitialized: boolean = false + _e2ePluginsInitialized: boolean = false + constructor (private _projectRoot: string, protected ctx: BaseContext) {} @nxs.field.nonNull.id() - id (): NxsResult<'LocalProject', 'id'> { + id (): NxsResult<'Project', 'id'> { return this.projectRoot } @nxs.field.nonNull.string() - title (): NxsResult<'LocalProject', 'title'> { + title (): NxsResult<'Project', 'title'> { return 'Title' } @nxs.field.string({ description: 'Used to associate project with Cypress cloud', }) - async projectId (): Promise> { - return await this.ctx.actions.getProjectId(this.projectRoot) + async projectId (): Promise> { + try { + return await this.ctx.actions.getProjectId(this.projectRoot) + } catch (e) { + // eslint-disable-next-line + console.error(e) + + return null + } } @nxs.field.nonNull.string() - get projectRoot (): NxsResult<'LocalProject', 'projectRoot'> { + get projectRoot (): NxsResult<'Project', 'projectRoot'> { return this._projectRoot } + + @nxs.field.type(() => ResolvedConfig) + resolvedConfig (): NxsResult<'Project', 'resolvedConfig'> { + const cfg = this.ctx.actions.resolveOpenProjectConfig() + + if (!cfg) { + throw Error('openProject.getConfig is null. Have you initialized the current project?') + } + + return new ResolvedConfig(cfg.resolved) + } + + @nxs.field.type(() => 'CloudProject') + async cloudProject (args: unknown, ctx: NxsCtx, info: GraphQLResolveInfo): Promise> { + // TODO: Tim: fix this, we shouldn't be awaiting projectId here + const projId = await this.projectId() + + if (projId) { + return this.ctx.cloudProjectsBySlug(projId, info) as any + } + + return null + } + + @nxs.field.nonNull.boolean({ + description: 'Whether the user configured this project to use Component Testing', + }) + get isFirstTimeCT (): NxsResult<'Project', 'isFirstTimeCT'> { + return this.ctx.actions.isFirstTime(this.projectRoot, 'component') + } + + @nxs.field.nonNull.boolean({ + description: 'Whether the user configured this project to use e2e Testing', + }) + get isFirstTimeE2E (): NxsResult<'Project', 'isFirstTimeE2E'> { + return this.ctx.actions.isFirstTime(this.projectRoot, 'e2e') + } + + setE2EPluginsInitialized (init: boolean): void { + this._e2ePluginsInitialized = init + } + + get e2ePluginsInitialized (): boolean { + return this._e2ePluginsInitialized + } + + setCtPluginsInitialized (init: boolean): void { + this._ctPluginsInitialized = init + } + + get ctPluginsInitialized (): boolean { + return this._ctPluginsInitialized + } } diff --git a/packages/graphql/src/entities/Query.ts b/packages/graphql/src/entities/Query.ts index 4425adb04602..cb67d6b90f62 100644 --- a/packages/graphql/src/entities/Query.ts +++ b/packages/graphql/src/entities/Query.ts @@ -1,8 +1,8 @@ -import { nxs, NxsQueryResult } from 'nexus-decorators' +import type { GraphQLResolveInfo } from 'graphql' +import { nxs, NxsCtx, NxsQueryResult } from 'nexus-decorators' import type { NexusGenTypes } from '../gen/nxs.gen' import { App } from './App' import { NavigationMenu } from './NavigationMenu' -import { Viewer } from './Viewer' import { Wizard } from './Wizard' @nxs.objectType({ @@ -14,13 +14,6 @@ export class Query { return ctx.app } - @nxs.field.type(() => Viewer, { - description: 'Namespace for data accessible from Cypress Cloud for authenticated users', - }) - viewer (args: unknown, ctx: NexusGenTypes['context']): NxsQueryResult<'viewer'> { - return ctx.viewer ?? null - } - @nxs.field.nonNull.type(() => Wizard, { description: 'Metadata about the wizard, null if we arent showing the wizard', }) @@ -34,4 +27,8 @@ export class Query { navigationMenu (args: unknown, ctx: NexusGenTypes['context']): NxsQueryResult<'navigationMenu'> { return ctx.navigationMenu } + + cloudViewer (args: unknown, ctx: NxsCtx, info: GraphQLResolveInfo): Promise | null> { + return ctx.delegateToRemoteQuery<'CloudUser'>(info) + } } diff --git a/packages/graphql/src/entities/ResolvedConfig.ts b/packages/graphql/src/entities/ResolvedConfig.ts index 34728b562d04..d6fbd5847275 100644 --- a/packages/graphql/src/entities/ResolvedConfig.ts +++ b/packages/graphql/src/entities/ResolvedConfig.ts @@ -19,11 +19,14 @@ const ResolvedTypeEnum = enumType({ members: RESOLVED_TYPE, }) -@nxs.objectType() +@nxs.interfaceType() export abstract class ResolvedOptionBase { constructor (protected resolveFromConfig: ResolvedFromConfig) {} - abstract get type (): ResolvedType + @nxs.field.nonNull.type(() => ResolvedTypeEnum) + get type (): ResolvedType { + throw new Error('Abstract') + } @nxs.field.type(() => ResolvedConfigOptionEnum) get from (): ResolvedFromConfig['from'] { diff --git a/packages/graphql/src/entities/Viewer.ts b/packages/graphql/src/entities/Viewer.ts deleted file mode 100644 index e2cceca753b4..000000000000 --- a/packages/graphql/src/entities/Viewer.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { nonNull, stringArg } from 'nexus' -import { nxs, NxsResult } from 'nexus-decorators' -import type { BaseContext } from '../context/BaseContext' -import type { NexusGenArgTypes } from '../gen/nxs.gen' -import { DashboardProject } from './DashboardProject' - -export interface AuthenticatedUser { - name: string - email: string - authToken: string -} - -@nxs.objectType({ - description: 'Namespace for information related to the viewer', -}) -export class Viewer { - constructor (private ctx: BaseContext, private viewer: AuthenticatedUser) {} - - // @nxs.field.nullable.type(() => DashboardProject, { - @nxs.field.nullable.type(() => DashboardProject, { - description: 'Active project', - args: { - projectId: nonNull(stringArg()), - }, - }) - getProjectByProjectId ({ projectId }: NexusGenArgTypes['Viewer']['getProjectByProjectId']): NxsResult<'Viewer', 'getProjectByProjectId'> { - const project = this.ctx.localProjects.find(async (p) => { - return await p.projectId() === projectId - }) - - if (!project) { - return null - } - - return new DashboardProject(project.projectRoot, this.ctx, this.viewer.authToken) - } - - @nxs.field.list.nullable.type(() => DashboardProject, { - description: 'All known projects for the app', - }) - get projects (): NxsResult<'Viewer', 'projects'> { - if (!this.authToken) { - return null - } - - return this.ctx.localProjects.map((p) => { - return new DashboardProject(p.projectRoot, this.ctx, this.authToken!) - }) - } - - @nxs.field.nonNull.string() - get name (): NxsResult<'Viewer', 'name'> { - return this.viewer.name - } - - @nxs.field.nonNull.string() - get email (): NxsResult<'Viewer', 'email'> { - return this.viewer.email - } - - @nxs.field.nonNull.string() - get authToken (): NxsResult<'Viewer', 'authToken'> { - return this.viewer.authToken - } -} diff --git a/packages/graphql/src/entities/index.ts b/packages/graphql/src/entities/index.ts index 1d23ba5fcae5..9883da5d51bd 100644 --- a/packages/graphql/src/entities/index.ts +++ b/packages/graphql/src/entities/index.ts @@ -3,8 +3,6 @@ export * from './App' export * from './Browser' -export * from './DashboardProject' -export * from './LocalProject' export * from './Mutation' export * from './NavigationItem' export * from './NavigationMenu' @@ -12,9 +10,7 @@ export * from './Project' export * from './Query' export * from './ResolvedConfig' export * from './TestingTypeInfo' -export * from './Viewer' export * from './Wizard' export * from './WizardBundler' export * from './WizardFrontendFramework' export * from './WizardNpmPackage' -export * from './run/' diff --git a/packages/graphql/src/entities/run/Run.ts b/packages/graphql/src/entities/run/Run.ts deleted file mode 100644 index 8851c622cfc7..000000000000 --- a/packages/graphql/src/entities/run/Run.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { nxs, NxsResult } from 'nexus-decorators' -import { RunGroupStatus, RunGroupStatusEnum } from '../../constants' -import { RunCommit } from './RunCommit' - -export interface RunCommitConfig { - authorEmail: string - authorName: string - branch: string - message: string - sha: string - url: string -} - -export interface RunGroupTotals { - totalPassed: number | null - totalFailed: number | null - totalPending: number | null - totalSkipped: number | null - totalDuration: number | null -} - -export interface RunGroupConfig extends RunGroupTotals { - status: RunGroupStatus - id: string - completedAt: string - createdAt: string - commit: RunCommitConfig -} - -@nxs.objectType({ - description: 'Represents a run on Cypress Cloud', -}) -export class RunGroup { - constructor (private config: RunGroupConfig) {} - - @nxs.field.nonNull.type(() => RunGroupStatusEnum) - get status (): NxsResult<'RunGroup', 'status'> { - return this.config.status - } - - @nxs.field.nonNull.string() - get createdAt (): NxsResult<'RunGroup', 'createdAt'> { - return this.config.createdAt - } - - @nxs.field.nonNull.string() - get completedAt (): NxsResult<'RunGroup', 'completedAt'> { - return this.config.completedAt - } - - @nxs.field.int() - get totalPassed (): NxsResult<'RunGroup', 'totalPassed'> { - return this.config.totalPassed ?? null - } - - @nxs.field.int() - get totalFailed (): NxsResult<'RunGroup', 'totalFailed'> { - return this.config.totalFailed ?? null - } - - @nxs.field.int() - get totalPending (): NxsResult<'RunGroup', 'totalPending'> { - return this.config.totalPending ?? null - } - - @nxs.field.int() - get totalSkipped (): NxsResult<'RunGroup', 'totalSkipped'> { - return this.config.totalSkipped ?? null - } - - @nxs.field.int() - get totalDuration (): NxsResult<'RunGroup', 'totalDuration'> { - return this.config.totalDuration ?? null - } - - @nxs.field.nonNull.type(() => RunCommit) - get commit (): NxsResult<'RunGroup', 'commit'> { - return new RunCommit(this.config.commit) - } -} diff --git a/packages/graphql/src/entities/run/RunCommit.ts b/packages/graphql/src/entities/run/RunCommit.ts deleted file mode 100644 index 6e7dfa8ea480..000000000000 --- a/packages/graphql/src/entities/run/RunCommit.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { nxs, NxsResult } from 'nexus-decorators' -import type { RunCommitConfig } from './Run' - -@nxs.objectType({ - description: 'Represents a commit on run on Cypress Cloud', -}) -export class RunCommit { - constructor (private config: RunCommitConfig) {} - - @nxs.field.nonNull.string() - get authorName (): NxsResult<'RunCommit', 'authorName'> { - return this.config.authorName - } - - @nxs.field.nonNull.string() - get authorEmail (): NxsResult<'RunCommit', 'authorEmail'> { - return this.config.authorEmail - } - - @nxs.field.nonNull.string() - get branch (): NxsResult<'RunCommit', 'branch'> { - return this.config.branch - } - - @nxs.field.nonNull.string() - get message (): NxsResult<'RunCommit', 'message'> { - return this.config.message - } - - @nxs.field.nonNull.string() - get sha (): NxsResult<'RunCommit', 'sha'> { - return this.config.sha - } - - @nxs.field.nonNull.string() - get url (): NxsResult<'RunCommit', 'url'> { - return this.config.url - } -} diff --git a/packages/graphql/src/entities/run/index.ts b/packages/graphql/src/entities/run/index.ts deleted file mode 100644 index 7873aef195b1..000000000000 --- a/packages/graphql/src/entities/run/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* eslint-disable padding-line-between-statements */ -// created by autobarrel, do not modify directly - -export * from './Run' -export * from './RunCommit' diff --git a/packages/graphql/src/gen/index.ts b/packages/graphql/src/gen/index.ts deleted file mode 100644 index 6d6afbcd7da7..000000000000 --- a/packages/graphql/src/gen/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* eslint-disable padding-line-between-statements */ -// created by autobarrel, do not modify directly - -export * from './nxs.gen' diff --git a/packages/graphql/src/gen/nxs.gen.ts b/packages/graphql/src/gen/nxs.gen.ts index fe54b1d8979d..3b353983f45a 100644 --- a/packages/graphql/src/gen/nxs.gen.ts +++ b/packages/graphql/src/gen/nxs.gen.ts @@ -4,25 +4,20 @@ * Do not make changes to this file directly */ - +import type * as cloudGen from "./cloud-source-types.gen" import type { BaseContext } from "./../context/BaseContext" +import type { Query } from "./../entities/Query" import type { App } from "./../entities/App" import type { Browser } from "./../entities/Browser" -import type { DashboardProject } from "./../entities/DashboardProject" -import type { LocalProject } from "./../entities/LocalProject" import type { NavigationItem } from "./../entities/NavigationItem" import type { NavigationMenu } from "./../entities/NavigationMenu" import type { Project } from "./../entities/Project" -import type { Query } from "./../entities/Query" import type { ResolvedOptionBase, ResolvedStringOption, ResolvedStringListOption, ResolvedNumberOption, ResolvedBooleanOption, ResolvedJsonOption, ResolvedConfig } from "./../entities/ResolvedConfig" import type { TestingTypeInfo } from "./../entities/TestingTypeInfo" -import type { Viewer } from "./../entities/Viewer" import type { Wizard } from "./../entities/Wizard" import type { WizardBundler } from "./../entities/WizardBundler" import type { WizardFrontendFramework } from "./../entities/WizardFrontendFramework" import type { WizardNpmPackage } from "./../entities/WizardNpmPackage" -import type { RunGroup } from "./../entities/run/Run" -import type { RunCommit } from "./../entities/run/RunCommit" import type { core } from "nexus" declare global { interface NexusGenCustomInputMethods { @@ -59,12 +54,12 @@ export interface NexusGenInputs { export interface NexusGenEnums { BrowserFamily: "chromium" | "firefox" + CloudRunStatus: cloudGen.CloudRunStatus FrontendFramework: "cra" | "nextjs" | "nuxtjs" | "react" | "vue" | "vuecli" NavItem: "learn" | "projectSetup" | "runs" | "settings" PluginsState: "error" | "initialized" | "initializing" | "uninitialized" ResolvedConfigOption: "config" | "default" | "env" | "plugin" | "runtime" ResolvedType: "array" | "boolean" | "json" | "number" | "string" - RunGroupStatus: "cancelled" | "errored" | "failed" | "noTests" | "passed" | "running" | "timedOut" | "unclaimed" SupportedBundlers: "vite" | "webpack" TestingTypeEnum: "component" | "e2e" WizardCodeLanguage: "js" | "ts" @@ -78,6 +73,7 @@ export interface NexusGenScalars { Float: number Boolean: boolean ID: string + Date: any DateTime: any JSON: any } @@ -85,24 +81,31 @@ export interface NexusGenScalars { export interface NexusGenObjects { App: App; Browser: Browser; - DashboardProject: DashboardProject; - LocalProject: LocalProject; + CloudOrganization: cloudGen.CloudOrganization; + CloudOrganizationConnection: cloudGen.CloudOrganizationConnection; + CloudOrganizationEdge: cloudGen.CloudOrganizationEdge; + CloudProject: cloudGen.CloudProject; + CloudProjectConnection: cloudGen.CloudProjectConnection; + CloudProjectEdge: cloudGen.CloudProjectEdge; + CloudRecordKey: cloudGen.CloudRecordKey; + CloudRun: cloudGen.CloudRun; + CloudRunCommitInfo: cloudGen.CloudRunCommitInfo; + CloudRunConnection: cloudGen.CloudRunConnection; + CloudRunEdge: cloudGen.CloudRunEdge; + CloudUser: cloudGen.CloudUser; Mutation: {}; NavigationItem: NavigationItem; NavigationMenu: NavigationMenu; + PageInfo: cloudGen.PageInfo; Project: Project; Query: Query; ResolvedBooleanOption: ResolvedBooleanOption; ResolvedConfig: ResolvedConfig; ResolvedJsonOption: ResolvedJsonOption; ResolvedNumberOption: ResolvedNumberOption; - ResolvedOptionBase: ResolvedOptionBase; ResolvedStringListOption: ResolvedStringListOption; ResolvedStringOption: ResolvedStringOption; - RunCommit: RunCommit; - RunGroup: RunGroup; TestingTypeInfo: TestingTypeInfo; - Viewer: Viewer; Wizard: Wizard; WizardBundler: WizardBundler; WizardFrontendFramework: WizardFrontendFramework; @@ -110,60 +113,123 @@ export interface NexusGenObjects { } export interface NexusGenInterfaces { + Node: cloudGen.Node; + ResolvedOptionBase: ResolvedOptionBase; } export interface NexusGenUnions { } -export type NexusGenRootTypes = NexusGenObjects +export type NexusGenRootTypes = NexusGenInterfaces & NexusGenObjects export type NexusGenAllTypes = NexusGenRootTypes & NexusGenScalars & NexusGenEnums export interface NexusGenFieldTypes { App: { // field return type - activeProject: NexusGenRootTypes['LocalProject'] | null; // LocalProject + activeProject: NexusGenRootTypes['Project'] | null; // Project browsers: NexusGenRootTypes['Browser'][]; // [Browser!]! healthCheck: string; // String! isFirstOpen: boolean; // Boolean! isInGlobalMode: boolean; // Boolean! - projects: NexusGenRootTypes['LocalProject'][]; // [LocalProject!]! + projects: NexusGenRootTypes['Project'][]; // [Project!]! } Browser: { // field return type channel: string; // String! + disabled: boolean; // Boolean! displayName: string; // String! family: NexusGenEnums['BrowserFamily']; // BrowserFamily! + id: string; // String! majorVersion: string | null; // String name: string; // String! path: string; // String! version: string; // String! } - DashboardProject: { // field return type + CloudOrganization: { // field return type id: string; // ID! - projectId: string | null; // String - projectRoot: string; // String! - recordKeys: string[] | null; // [String!] - runs: NexusGenRootTypes['RunGroup'][] | null; // [RunGroup!] - title: string; // String! + name: string | null; // String + projects: NexusGenRootTypes['CloudProjectConnection'] | null; // CloudProjectConnection } - LocalProject: { // field return type + CloudOrganizationConnection: { // field return type + edges: NexusGenRootTypes['CloudOrganizationEdge'][]; // [CloudOrganizationEdge!]! + nodes: NexusGenRootTypes['CloudOrganization'][]; // [CloudOrganization!]! + pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo! + } + CloudOrganizationEdge: { // field return type + cursor: string; // String! + node: NexusGenRootTypes['CloudOrganization']; // CloudOrganization! + } + CloudProject: { // field return type id: string; // ID! - isFirstTimeCT: boolean; // Boolean! - isFirstTimeE2E: boolean; // Boolean! - projectId: string | null; // String - projectRoot: string; // String! - resolvedConfig: NexusGenRootTypes['ResolvedConfig'] | null; // ResolvedConfig - title: string; // String! + latestRun: NexusGenRootTypes['CloudRun'] | null; // CloudRun + organization: NexusGenRootTypes['CloudOrganization'] | null; // CloudOrganization + recordKeys: NexusGenRootTypes['CloudRecordKey'][] | null; // [CloudRecordKey!] + runs: NexusGenRootTypes['CloudRunConnection'] | null; // CloudRunConnection + slug: string; // String! + } + CloudProjectConnection: { // field return type + edges: NexusGenRootTypes['CloudProjectEdge'][]; // [CloudProjectEdge!]! + nodes: NexusGenRootTypes['CloudProject'][]; // [CloudProject!]! + pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo! + } + CloudProjectEdge: { // field return type + cursor: string; // String! + node: NexusGenRootTypes['CloudProject']; // CloudProject! + } + CloudRecordKey: { // field return type + createdAt: NexusGenScalars['DateTime'] | null; // DateTime + id: string; // ID! + key: string | null; // String + lastUsedAt: NexusGenScalars['DateTime'] | null; // DateTime + } + CloudRun: { // field return type + commitInfo: NexusGenRootTypes['CloudRunCommitInfo'] | null; // CloudRunCommitInfo + createdAt: NexusGenScalars['Date'] | null; // Date + id: string; // ID! + status: NexusGenEnums['CloudRunStatus'] | null; // CloudRunStatus + totalDuration: number | null; // Int + totalFailed: number | null; // Int + totalPassed: number | null; // Int + totalPending: number | null; // Int + totalRunning: number | null; // Int + totalSkipped: number | null; // Int + totalTests: number | null; // Int + } + CloudRunCommitInfo: { // field return type + authorAvatar: string | null; // String + authorEmail: string | null; // String + authorName: string | null; // String + branch: string | null; // String + branchUrl: string | null; // String + message: string | null; // String + sha: string | null; // String + summary: string | null; // String + url: string | null; // String + } + CloudRunConnection: { // field return type + edges: NexusGenRootTypes['CloudRunEdge'][]; // [CloudRunEdge!]! + nodes: NexusGenRootTypes['CloudRun'][]; // [CloudRun!]! + pageInfo: NexusGenRootTypes['PageInfo']; // PageInfo! + } + CloudRunEdge: { // field return type + cursor: string; // String! + node: NexusGenRootTypes['CloudRun']; // CloudRun! + } + CloudUser: { // field return type + email: string | null; // String + fullName: string | null; // String + id: string; // ID! + organizations: NexusGenRootTypes['CloudOrganizationConnection'] | null; // CloudOrganizationConnection + userIsViewer: boolean; // Boolean! } Mutation: { // field return type appCreateConfigFile: NexusGenRootTypes['App'] | null; // App initializeOpenProject: NexusGenRootTypes['Wizard'] | null; // Wizard launchOpenProject: NexusGenRootTypes['App'] | null; // App - login: NexusGenRootTypes['Viewer'] | null; // Viewer - logout: NexusGenRootTypes['Viewer'] | null; // Viewer + login: NexusGenRootTypes['Query'] | null; // Query + logout: NexusGenRootTypes['Query'] | null; // Query navigationMenuSetItem: NexusGenRootTypes['NavigationMenu'] | null; // NavigationMenu wizardInstallDependencies: NexusGenRootTypes['Wizard'] | null; // Wizard wizardNavigate: NexusGenRootTypes['Wizard'] | null; // Wizard - wizardNavigateForward: NexusGenRootTypes['Wizard'] | null; // Wizard wizardSetBundler: NexusGenRootTypes['Wizard'] | null; // Wizard wizardSetFramework: NexusGenRootTypes['Wizard'] | null; // Wizard wizardSetManualInstall: NexusGenRootTypes['Wizard'] | null; // Wizard @@ -177,19 +243,32 @@ export interface NexusGenFieldTypes { selected: boolean; // Boolean! } NavigationMenu: { // field return type - items: Array; // [NavigationItem]! + items: NexusGenRootTypes['NavigationItem'][]; // [NavigationItem!]! selected: NexusGenEnums['NavItem']; // NavItem! } + PageInfo: { // field return type + endCursor: string | null; // String + hasNextPage: boolean; // Boolean! + hasPreviousPage: boolean; // Boolean! + startCursor: string | null; // String + } Project: { // field return type + cloudProject: NexusGenRootTypes['CloudProject'] | null; // CloudProject id: string; // ID! + isFirstTimeCT: boolean; // Boolean! + isFirstTimeE2E: boolean; // Boolean! projectId: string | null; // String projectRoot: string; // String! + resolvedConfig: NexusGenRootTypes['ResolvedConfig'] | null; // ResolvedConfig title: string; // String! } Query: { // field return type app: NexusGenRootTypes['App']; // App! + cloudNode: NexusGenRootTypes['Node'] | null; // Node + cloudProjectBySlug: NexusGenRootTypes['CloudProject'] | null; // CloudProject + cloudProjectsBySlugs: Array | null; // [CloudProject] + cloudViewer: NexusGenRootTypes['CloudUser'] | null; // CloudUser navigationMenu: NexusGenRootTypes['NavigationMenu'] | null; // NavigationMenu - viewer: NexusGenRootTypes['Viewer'] | null; // Viewer wizard: NexusGenRootTypes['Wizard']; // Wizard! } ResolvedBooleanOption: { // field return type @@ -259,9 +338,6 @@ export interface NexusGenFieldTypes { type: NexusGenEnums['ResolvedType']; // ResolvedType! value: string | null; // String } - ResolvedOptionBase: { // field return type - from: NexusGenEnums['ResolvedConfigOption'] | null; // ResolvedConfigOption - } ResolvedStringListOption: { // field return type from: NexusGenEnums['ResolvedConfigOption'] | null; // ResolvedConfigOption type: NexusGenEnums['ResolvedType']; // ResolvedType! @@ -272,37 +348,11 @@ export interface NexusGenFieldTypes { type: NexusGenEnums['ResolvedType']; // ResolvedType! value: string | null; // String } - RunCommit: { // field return type - authorEmail: string; // String! - authorName: string; // String! - branch: string; // String! - message: string; // String! - sha: string; // String! - url: string; // String! - } - RunGroup: { // field return type - commit: NexusGenRootTypes['RunCommit']; // RunCommit! - completedAt: string; // String! - createdAt: string; // String! - status: NexusGenEnums['RunGroupStatus']; // RunGroupStatus! - totalDuration: number | null; // Int - totalFailed: number | null; // Int - totalPassed: number | null; // Int - totalPending: number | null; // Int - totalSkipped: number | null; // Int - } TestingTypeInfo: { // field return type description: string; // String! id: NexusGenEnums['TestingTypeEnum']; // TestingTypeEnum! title: string; // String! } - Viewer: { // field return type - authToken: string; // String! - email: string; // String! - getProjectByProjectId: NexusGenRootTypes['DashboardProject'] | null; // DashboardProject - name: string; // String! - projects: Array | null; // [DashboardProject] - } Wizard: { // field return type allBundlers: NexusGenRootTypes['WizardBundler'][]; // [WizardBundler!]! bundler: NexusGenRootTypes['WizardBundler'] | null; // WizardBundler @@ -334,53 +384,121 @@ export interface NexusGenFieldTypes { description: string; // String! name: string; // String! } + Node: { // field return type + id: string; // ID! + } + ResolvedOptionBase: { // field return type + from: NexusGenEnums['ResolvedConfigOption'] | null; // ResolvedConfigOption + type: NexusGenEnums['ResolvedType']; // ResolvedType! + } } export interface NexusGenFieldTypeNames { App: { // field return type name - activeProject: 'LocalProject' + activeProject: 'Project' browsers: 'Browser' healthCheck: 'String' isFirstOpen: 'Boolean' isInGlobalMode: 'Boolean' - projects: 'LocalProject' + projects: 'Project' } Browser: { // field return type name channel: 'String' + disabled: 'Boolean' displayName: 'String' family: 'BrowserFamily' + id: 'String' majorVersion: 'String' name: 'String' path: 'String' version: 'String' } - DashboardProject: { // field return type name + CloudOrganization: { // field return type name id: 'ID' - projectId: 'String' - projectRoot: 'String' - recordKeys: 'String' - runs: 'RunGroup' - title: 'String' + name: 'String' + projects: 'CloudProjectConnection' + } + CloudOrganizationConnection: { // field return type name + edges: 'CloudOrganizationEdge' + nodes: 'CloudOrganization' + pageInfo: 'PageInfo' + } + CloudOrganizationEdge: { // field return type name + cursor: 'String' + node: 'CloudOrganization' } - LocalProject: { // field return type name + CloudProject: { // field return type name id: 'ID' - isFirstTimeCT: 'Boolean' - isFirstTimeE2E: 'Boolean' - projectId: 'String' - projectRoot: 'String' - resolvedConfig: 'ResolvedConfig' - title: 'String' + latestRun: 'CloudRun' + organization: 'CloudOrganization' + recordKeys: 'CloudRecordKey' + runs: 'CloudRunConnection' + slug: 'String' + } + CloudProjectConnection: { // field return type name + edges: 'CloudProjectEdge' + nodes: 'CloudProject' + pageInfo: 'PageInfo' + } + CloudProjectEdge: { // field return type name + cursor: 'String' + node: 'CloudProject' + } + CloudRecordKey: { // field return type name + createdAt: 'DateTime' + id: 'ID' + key: 'String' + lastUsedAt: 'DateTime' + } + CloudRun: { // field return type name + commitInfo: 'CloudRunCommitInfo' + createdAt: 'Date' + id: 'ID' + status: 'CloudRunStatus' + totalDuration: 'Int' + totalFailed: 'Int' + totalPassed: 'Int' + totalPending: 'Int' + totalRunning: 'Int' + totalSkipped: 'Int' + totalTests: 'Int' + } + CloudRunCommitInfo: { // field return type name + authorAvatar: 'String' + authorEmail: 'String' + authorName: 'String' + branch: 'String' + branchUrl: 'String' + message: 'String' + sha: 'String' + summary: 'String' + url: 'String' + } + CloudRunConnection: { // field return type name + edges: 'CloudRunEdge' + nodes: 'CloudRun' + pageInfo: 'PageInfo' + } + CloudRunEdge: { // field return type name + cursor: 'String' + node: 'CloudRun' + } + CloudUser: { // field return type name + email: 'String' + fullName: 'String' + id: 'ID' + organizations: 'CloudOrganizationConnection' + userIsViewer: 'Boolean' } Mutation: { // field return type name appCreateConfigFile: 'App' initializeOpenProject: 'Wizard' launchOpenProject: 'App' - login: 'Viewer' - logout: 'Viewer' + login: 'Query' + logout: 'Query' navigationMenuSetItem: 'NavigationMenu' wizardInstallDependencies: 'Wizard' wizardNavigate: 'Wizard' - wizardNavigateForward: 'Wizard' wizardSetBundler: 'Wizard' wizardSetFramework: 'Wizard' wizardSetManualInstall: 'Wizard' @@ -397,16 +515,29 @@ export interface NexusGenFieldTypeNames { items: 'NavigationItem' selected: 'NavItem' } + PageInfo: { // field return type name + endCursor: 'String' + hasNextPage: 'Boolean' + hasPreviousPage: 'Boolean' + startCursor: 'String' + } Project: { // field return type name + cloudProject: 'CloudProject' id: 'ID' + isFirstTimeCT: 'Boolean' + isFirstTimeE2E: 'Boolean' projectId: 'String' projectRoot: 'String' + resolvedConfig: 'ResolvedConfig' title: 'String' } Query: { // field return type name app: 'App' + cloudNode: 'Node' + cloudProjectBySlug: 'CloudProject' + cloudProjectsBySlugs: 'CloudProject' + cloudViewer: 'CloudUser' navigationMenu: 'NavigationMenu' - viewer: 'Viewer' wizard: 'Wizard' } ResolvedBooleanOption: { // field return type name @@ -476,9 +607,6 @@ export interface NexusGenFieldTypeNames { type: 'ResolvedType' value: 'String' } - ResolvedOptionBase: { // field return type name - from: 'ResolvedConfigOption' - } ResolvedStringListOption: { // field return type name from: 'ResolvedConfigOption' type: 'ResolvedType' @@ -489,37 +617,11 @@ export interface NexusGenFieldTypeNames { type: 'ResolvedType' value: 'String' } - RunCommit: { // field return type name - authorEmail: 'String' - authorName: 'String' - branch: 'String' - message: 'String' - sha: 'String' - url: 'String' - } - RunGroup: { // field return type name - commit: 'RunCommit' - completedAt: 'String' - createdAt: 'String' - status: 'RunGroupStatus' - totalDuration: 'Int' - totalFailed: 'Int' - totalPassed: 'Int' - totalPending: 'Int' - totalSkipped: 'Int' - } TestingTypeInfo: { // field return type name description: 'String' id: 'TestingTypeEnum' title: 'String' } - Viewer: { // field return type name - authToken: 'String' - email: 'String' - getProjectByProjectId: 'DashboardProject' - name: 'String' - projects: 'DashboardProject' - } Wizard: { // field return type name allBundlers: 'WizardBundler' bundler: 'WizardBundler' @@ -551,9 +653,47 @@ export interface NexusGenFieldTypeNames { description: 'String' name: 'String' } + Node: { // field return type name + id: 'ID' + } + ResolvedOptionBase: { // field return type name + from: 'ResolvedConfigOption' + type: 'ResolvedType' + } } export interface NexusGenArgTypes { + CloudOrganization: { + projects: { // args + after?: string | null; // String + before?: string | null; // String + first?: number | null; // Int + last?: number | null; // Int + } + } + CloudProject: { + runs: { // args + after?: string | null; // String + before?: string | null; // String + cypressVersion?: string | null; // String + first?: number | null; // Int + last?: number | null; // Int + status?: NexusGenEnums['CloudRunStatus'] | null; // CloudRunStatus + } + } + CloudRunCommitInfo: { + message: { // args + truncate?: number | null; // Int + } + } + CloudUser: { + organizations: { // args + after?: string | null; // String + before?: string | null; // String + first?: number | null; // Int + last?: number | null; // Int + } + } Mutation: { appCreateConfigFile: { // args code: string; // String! @@ -578,9 +718,15 @@ export interface NexusGenArgTypes { type: NexusGenEnums['TestingTypeEnum']; // TestingTypeEnum! } } - Viewer: { - getProjectByProjectId: { // args - projectId: string; // String! + Query: { + cloudNode: { // args + id: string; // ID! + } + cloudProjectBySlug: { // args + slug: string; // String! + } + cloudProjectsBySlugs: { // args + slugs: string[]; // [String!]! } } Wizard: { @@ -591,9 +737,21 @@ export interface NexusGenArgTypes { } export interface NexusGenAbstractTypeMembers { + Node: "CloudOrganization" | "CloudProject" | "CloudRecordKey" | "CloudRun" | "CloudUser" + ResolvedOptionBase: "ResolvedBooleanOption" | "ResolvedJsonOption" | "ResolvedNumberOption" | "ResolvedStringListOption" | "ResolvedStringOption" } export interface NexusGenTypeInterfaces { + CloudOrganization: "Node" + CloudProject: "Node" + CloudRecordKey: "Node" + CloudRun: "Node" + CloudUser: "Node" + ResolvedBooleanOption: "ResolvedOptionBase" + ResolvedJsonOption: "ResolvedOptionBase" + ResolvedNumberOption: "ResolvedOptionBase" + ResolvedStringListOption: "ResolvedOptionBase" + ResolvedStringOption: "ResolvedOptionBase" } export type NexusGenObjectNames = keyof NexusGenObjects; @@ -602,7 +760,7 @@ export type NexusGenInputNames = never; export type NexusGenEnumNames = keyof NexusGenEnums; -export type NexusGenInterfaceNames = never; +export type NexusGenInterfaceNames = keyof NexusGenInterfaces; export type NexusGenScalarNames = keyof NexusGenScalars; @@ -610,7 +768,7 @@ export type NexusGenUnionNames = never; export type NexusGenObjectsUsingAbstractStrategyIsTypeOf = never; -export type NexusGenAbstractsUsingStrategyResolveType = never; +export type NexusGenAbstractsUsingStrategyResolveType = "Node"; export type NexusGenFeaturesConfig = { abstractTypeStrategies: { diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 6b6f30865798..7bc4c9ce959f 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -10,4 +10,6 @@ export { graphqlSchema } from './schema' export * from './contracts' -export { execute, parse } from 'graphql' +export { execute, parse, print } from 'graphql' + +export { remoteSchemaWrapped } from './stitching/remoteSchemaWrapped' diff --git a/packages/graphql/src/schema.ts b/packages/graphql/src/schema.ts index e431ad3a1691..ccf75a5b2754 100644 --- a/packages/graphql/src/schema.ts +++ b/packages/graphql/src/schema.ts @@ -1,9 +1,11 @@ import { makeSchema, asNexusMethod } from 'nexus' import path from 'path' import { JSONResolver, DateTimeResolver } from 'graphql-scalars' + import * as entities from './entities' import * as constants from './constants' import * as testingTypes from './testing/testUnionType' +import { remoteSchema } from './stitching/remoteSchema' const customScalars = [ asNexusMethod(JSONResolver, 'json'), @@ -12,25 +14,42 @@ const customScalars = [ // for vite const dirname = typeof __dirname !== 'undefined' ? __dirname : '' +const isVite = !dirname // for vite process.cwd ??= () => '' const isCodegen = Boolean(process.env.CYPRESS_INTERNAL_NEXUS_CODEGEN) +const types = [entities, constants, customScalars, isVite ? testingTypes : null] + export const graphqlSchema = makeSchema({ - types: [entities, constants, customScalars, dirname ? null : testingTypes], + types, shouldGenerateArtifacts: isCodegen, shouldExitAfterGenerateArtifacts: isCodegen, + sourceTypes: isCodegen ? { + modules: [ + { + alias: 'cloudGen', + module: path.join(dirname, 'gen/cloud-source-types.gen.ts'), + }, + ], + } : undefined, // for vite outputs: isCodegen ? { typegen: path.join(dirname, 'gen/nxs.gen.ts'), - schema: path.join(dirname, '..', 'schema.graphql'), + schema: path.join(dirname, '..', 'schemas', 'schema.graphql'), } : false, contextType: { module: path.join(dirname, './context/BaseContext.ts'), export: 'BaseContext', }, + mergeSchema: { + schema: remoteSchema, + skipFields: { + Mutation: ['test'], + }, + }, formatTypegen (content, type) { if (type === 'schema') { return content @@ -39,4 +58,7 @@ export const graphqlSchema = makeSchema({ // TODO(tim): fix in nexus to prevent the regex return `/* eslint-disable */\n${content.replace(/\.js"/g, '"')}` }, + features: { + abstractTypeRuntimeChecks: false, + }, }) diff --git a/packages/graphql/src/server.ts b/packages/graphql/src/server.ts index 5eb7de4678f2..9d51d8da4ec1 100644 --- a/packages/graphql/src/server.ts +++ b/packages/graphql/src/server.ts @@ -6,6 +6,7 @@ import type { AddressInfo } from 'net' import cors from 'cors' import { graphqlSchema } from './schema' import type { BaseContext } from './context/BaseContext' +import { Query } from './entities/Query' const debug = Debug('cypress:server:graphql') @@ -49,7 +50,7 @@ export function startGraphQLServer ({ port }: { port: number } = { port: 52159 } app.use(cors()) - app.use('/graphql', graphqlHTTP(() => { + app.use('/graphql', graphqlHTTP((req) => { if (!serverContext) { throw new Error(`setServerContext has not been called`) } @@ -58,6 +59,7 @@ export function startGraphQLServer ({ port }: { port: number } = { port: 52159 } schema: graphqlSchema, graphiql: true, context: serverContext, + rootValue: new Query(), } })) diff --git a/packages/graphql/src/stitching/remoteSchema.ts b/packages/graphql/src/stitching/remoteSchema.ts new file mode 100644 index 000000000000..fdb1c98eccd6 --- /dev/null +++ b/packages/graphql/src/stitching/remoteSchema.ts @@ -0,0 +1,18 @@ +/** + * DIY "Schema Stitching" + * + * Interleaves the remote GraphQL schema with the locally defined schema + * to create a single unified schema for fetching from the client. + */ +import { buildClientSchema } from 'graphql' +// import { arg, core, queryField } from 'nexus' + +// Using the introspection, since Vite doesn't like the fs.readFile +import introspectionResult from '../gen/cloud-introspection.gen.json' + +// Get the Remote schema we've sync'ed locally +export const remoteSchema = buildClientSchema( + // @ts-expect-error + introspectionResult, + { assumeValid: true }, +) diff --git a/packages/graphql/src/stitching/remoteSchemaExecutor.ts b/packages/graphql/src/stitching/remoteSchemaExecutor.ts new file mode 100644 index 000000000000..2befe8e2bfdd --- /dev/null +++ b/packages/graphql/src/stitching/remoteSchemaExecutor.ts @@ -0,0 +1,43 @@ +import type { Executor } from '@graphql-tools/utils/executor' +import { print } from 'graphql' +import fetch from 'cross-fetch' +import getenv from 'getenv' + +import type { BaseContext } from '../context/BaseContext' + +const cloudEnv = getenv('CYPRESS_INTERNAL_CLOUD_ENV', process.env.CYPRESS_INTERNAL_ENV) as keyof typeof REMOTE_SCHEMA_URLS +const REMOTE_SCHEMA_URLS = { + development: 'http://localhost:3000', + staging: 'https://dashboard-staging.cypress.io', + production: 'https://dashboard.cypress.io', +} + +/** + * Takes a "document" and executes it against the GraphQL schema + * @returns + */ +export const remoteSchemaExecutor: Executor = async ({ document, variables, context }) => { + if (!context?.authenticatedUser) { + return { data: null } + } + + const query = print(document) + + // TODO(tim): remove / change to debug + // eslint-disable-next-line + // console.log(`Executing query ${query} against remote`) + + const fetchResult = await fetch(`${REMOTE_SCHEMA_URLS[cloudEnv]}/test-runner-graphql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `bearer ${context?.authenticatedUser.authToken}`, + }, + body: JSON.stringify({ + query, + variables, + }), + }) + + return fetchResult.json() +} diff --git a/packages/graphql/src/stitching/remoteSchemaWrapped.ts b/packages/graphql/src/stitching/remoteSchemaWrapped.ts new file mode 100644 index 000000000000..104c86ab6c18 --- /dev/null +++ b/packages/graphql/src/stitching/remoteSchemaWrapped.ts @@ -0,0 +1,10 @@ +import { wrapSchema } from '@graphql-tools/wrap' +import { remoteSchema } from './remoteSchema' +import { remoteSchemaExecutor } from './remoteSchemaExecutor' + +// Takes the remote schema & wraps with an "executor", allowing us to delegate +// queries we know should be executed against this server +export const remoteSchemaWrapped = wrapSchema({ + schema: remoteSchema, + executor: remoteSchemaExecutor, +}) diff --git a/packages/graphql/src/testing/ProjectBaseTest.ts b/packages/graphql/src/testing/ProjectBaseTest.ts deleted file mode 100644 index 999504d1745f..000000000000 --- a/packages/graphql/src/testing/ProjectBaseTest.ts +++ /dev/null @@ -1 +0,0 @@ -export class ProjectBaseTest {} diff --git a/packages/graphql/src/testing/fake-uuid.d.ts b/packages/graphql/src/testing/fake-uuid.d.ts new file mode 100644 index 000000000000..b88f6a1cf20c --- /dev/null +++ b/packages/graphql/src/testing/fake-uuid.d.ts @@ -0,0 +1,5 @@ +declare module 'fake-uuid' { + type A_to_F = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' + const fakeUuid: (input?: A_to_F | number, fill?: number | A_to_F) => string + export default fakeUuid +} \ No newline at end of file diff --git a/packages/graphql/src/testing/index.ts b/packages/graphql/src/testing/index.ts deleted file mode 100644 index 89642059f39a..000000000000 --- a/packages/graphql/src/testing/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* eslint-disable padding-line-between-statements */ -// created by autobarrel, do not modify directly - -export * from './ProjectBaseTest' -export * from './testUnionType' diff --git a/packages/graphql/src/testing/remoteTestSchema.ts b/packages/graphql/src/testing/remoteTestSchema.ts new file mode 100644 index 000000000000..67d1bbd24dc9 --- /dev/null +++ b/packages/graphql/src/testing/remoteTestSchema.ts @@ -0,0 +1,35 @@ +import type { Executor } from '@graphql-tools/utils/executor' +import { print, graphql } from 'graphql' +import { wrapSchema } from '@graphql-tools/wrap' + +import { remoteSchema } from '../stitching/remoteSchema' +import { CloudRunQuery } from './testStubCloudTypes' + +export * from './testStubCloudTypes' + +const testExecutor: Executor = async ({ document, context }) => { + const result = await graphql({ + schema: remoteSchema, + rootValue: CloudRunQuery, + source: print(document), + contextValue: context, + }) as any + + return result +} + +export const remoteTestSchema = wrapSchema({ + schema: remoteSchema, + executor: testExecutor, +}) + +/** + * Mocks the "test context" behavior. Used from the frontend + */ +export class StubContext { + _remoteSchema = remoteTestSchema + + get localProjects () { + return [] + } +} diff --git a/packages/graphql/src/testing/testStubCloudTypes.ts b/packages/graphql/src/testing/testStubCloudTypes.ts new file mode 100644 index 000000000000..34faf699e436 --- /dev/null +++ b/packages/graphql/src/testing/testStubCloudTypes.ts @@ -0,0 +1,194 @@ +/* eslint-disable no-console */ +/** + * Takes all of the "Cloud" types and creates realistic mocks, + * indexing the types in a way that we can access them + */ +import _ from 'lodash' +import fakeUuid from 'fake-uuid' +import { connectionFromArray } from 'graphql-relay' + +import type { + Node, + CloudUser, + CloudRun, + CloudOrganization, + CloudRecordKey, + CloudRunCommitInfo, + CodegenTypeMap, + CloudProject, + Query, + QueryCloudNodeArgs, + QueryCloudProjectBySlugArgs, + QueryCloudProjectsBySlugsArgs, + CloudProjectRunsArgs, +} from '../gen/cloud-source-types.gen' +import { base64Encode } from '../util/relayConnectionUtils' +import type { NxsCtx } from 'nexus-decorators' +import type { GraphQLResolveInfo } from 'graphql' + +type ConfigFor = Omit + +export type CloudTypesWithId = { + [K in keyof CodegenTypeMap]: 'id' extends keyof CodegenTypeMap[K] ? K : never +}[keyof CodegenTypeMap] + +let nodeIdx: Partial> = {} + +function getNodeIdx (type: CloudTypesWithId): number { + return nodeIdx[type] ?? 0 +} + +function testNodeId (type: T) { + nodeIdx[type] = (nodeIdx[type] ?? 0) + 1 + + return { + __typename: type, + id: base64Encode(`${type}:${nodeIdx[type]}`), + } as const +} + +const nodeRegistry: Record = {} +const projectsBySlug: Record = {} + +function indexNode (node: T & {__typename: CloudTypesWithId}): T { + nodeRegistry[node.id] = node + if (node.__typename === 'CloudProject') { + // @ts-expect-error + projectsBySlug[node.slug] = node + } + + return node +} + +export function createCloudRunCommitInfo (config: ConfigFor): CloudRunCommitInfo { + const cloudRunCommitInfo: CloudRunCommitInfo = { + __typename: 'CloudRunCommitInfo', + sha: `d333752a1f168ccd92df2740fd640714067d243a`, + summary: 'calculate multi-byte panel', + branch: 'main', + branchUrl: 'https://', + } + + return cloudRunCommitInfo +} + +export function createCloudRecordKey (config: ConfigFor) { + const cloudRecordKey: CloudRecordKey = { + ...testNodeId('CloudRecordKey'), + key: fakeUuid(getNodeIdx('CloudRecordKey')), + ...config, + } + + return indexNode(cloudRecordKey) +} + +export function createCloudProject (config: ConfigFor) { + const cloudProject = { + ...testNodeId('CloudProject'), + recordKeys: [CloudRecordKeyStubs.componentProject], + latestRun: CloudRunStubs.running, + runs (args: CloudProjectRunsArgs) { + const twentyRuns = _.times(20, (i) => { + return createCloudRun({ + status: 'PASSED', + totalPassed: i, + commitInfo: createCloudRunCommitInfo({ sha: `fake-sha-${getNodeIdx('CloudRun')}` }), + }) + }) + + return { + ...connectionFromArray(twentyRuns, args), + nodes: twentyRuns, + } + }, + ...config, + } as CloudProject + + return indexNode(cloudProject) +} + +export function createCloudUser (config: ConfigFor): CloudUser { + const cloudUser: CloudUser = { + ...testNodeId('CloudUser'), + ...config, + } + + return indexNode(cloudUser) +} + +export function createCloudRun (config: Partial): CloudRun { + const cloudRunData: CloudRun = { + ...testNodeId('CloudRun'), + status: 'PASSED', + totalFailed: 0, + totalSkipped: 0, + totalRunning: 0, + totalTests: 10, + totalPassed: 10, + ...config, + createdAt: new Date().toISOString(), + } + + return indexNode(cloudRunData) +} + +export function createCloudOrganization (config: Partial): CloudOrganization { + const cloudOrgData: CloudOrganization = { + ...testNodeId('CloudOrganization'), + name: `Cypress Test Account ${getNodeIdx('CloudOrganization')}`, + ...config, + } + + return indexNode(cloudOrgData) +} + +export const CloudRecordKeyStubs = { + e2eProject: createCloudRecordKey({}), + componentProject: createCloudRecordKey({}), +} as const + +export const CloudRunCommitInfoStubs = { + commit: createCloudRunCommitInfo({}), +} as const + +export const CloudRunStubs = { + allPassing: createCloudRun({ status: 'PASSED', commitInfo: CloudRunCommitInfoStubs.commit }), + failing: createCloudRun({ status: 'FAILED', totalPassed: 8, totalFailed: 2 }), + running: createCloudRun({ status: 'RUNNING', totalRunning: 2, totalPassed: 8 }), + someSkipped: createCloudRun({ status: 'PASSED', totalPassed: 7, totalSkipped: 3 }), + allSkipped: createCloudRun({ status: 'ERRORED', totalPassed: 0, totalSkipped: 10 }), +} as const + +export const CloudUserStubs = { + me: createCloudUser({ userIsViewer: true }), + // meAsAdmin: createCloudUser({ userIsViewer: true }), TODO(tim): add when we have roles +} as const + +export const CloudOrganizationStubs = { + cyOrg: createCloudOrganization({}), +} as const + +export const CloudProjectStubs = { + e2eProject: createCloudProject({ slug: 'efgh' }), + componentProject: createCloudProject({ slug: 'abcd' }), +} as const + +type MaybeResolver = { + [K in keyof T]: K extends 'id' | '__typename' ? T[K] : T[K] | ((args: any, ctx: NxsCtx, info: GraphQLResolveInfo) => MaybeResolver) +} + +export const CloudRunQuery: MaybeResolver = { + __typename: 'Query', + cloudNode (args: QueryCloudNodeArgs) { + return nodeRegistry[args.id] ?? null + }, + cloudProjectBySlug (args: QueryCloudProjectBySlugArgs) { + return CloudProjectStubs.componentProject + }, + cloudProjectsBySlugs (args: QueryCloudProjectsBySlugsArgs) { + return args.slugs.map((s) => projectsBySlug[s] ?? null) + }, + cloudViewer (args, ctx) { + return CloudUserStubs.me + }, +} diff --git a/packages/graphql/src/testing/testUnionType.ts b/packages/graphql/src/testing/testUnionType.ts index d6757a922911..60d38b434609 100644 --- a/packages/graphql/src/testing/testUnionType.ts +++ b/packages/graphql/src/testing/testUnionType.ts @@ -1,27 +1,36 @@ /* eslint-disable */ // Generated by codegen-mount-ts.ts, do not edit directly -import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen' +import type { NexusGenObjects, NexusGenInterfaces } from '@packages/graphql/src/gen/nxs.gen' export interface TestSourceTypeLookup { App: NexusGenObjects['App'], Browser: NexusGenObjects['Browser'], - DashboardProject: NexusGenObjects['DashboardProject'], - LocalProject: NexusGenObjects['LocalProject'], + CloudOrganization: NexusGenObjects['CloudOrganization'], + CloudOrganizationConnection: NexusGenObjects['CloudOrganizationConnection'], + CloudOrganizationEdge: NexusGenObjects['CloudOrganizationEdge'], + CloudProject: NexusGenObjects['CloudProject'], + CloudProjectConnection: NexusGenObjects['CloudProjectConnection'], + CloudProjectEdge: NexusGenObjects['CloudProjectEdge'], + CloudRecordKey: NexusGenObjects['CloudRecordKey'], + CloudRun: NexusGenObjects['CloudRun'], + CloudRunCommitInfo: NexusGenObjects['CloudRunCommitInfo'], + CloudRunConnection: NexusGenObjects['CloudRunConnection'], + CloudRunEdge: NexusGenObjects['CloudRunEdge'], + CloudUser: NexusGenObjects['CloudUser'], Mutation: NexusGenObjects['Mutation'], NavigationItem: NexusGenObjects['NavigationItem'], NavigationMenu: NexusGenObjects['NavigationMenu'], + Node: NexusGenInterfaces['Node'], + PageInfo: NexusGenObjects['PageInfo'], Project: NexusGenObjects['Project'], Query: NexusGenObjects['Query'], ResolvedBooleanOption: NexusGenObjects['ResolvedBooleanOption'], ResolvedConfig: NexusGenObjects['ResolvedConfig'], ResolvedJsonOption: NexusGenObjects['ResolvedJsonOption'], ResolvedNumberOption: NexusGenObjects['ResolvedNumberOption'], - ResolvedOptionBase: NexusGenObjects['ResolvedOptionBase'], + ResolvedOptionBase: NexusGenInterfaces['ResolvedOptionBase'], ResolvedStringListOption: NexusGenObjects['ResolvedStringListOption'], ResolvedStringOption: NexusGenObjects['ResolvedStringOption'], - RunCommit: NexusGenObjects['RunCommit'], - RunGroup: NexusGenObjects['RunGroup'], TestingTypeInfo: NexusGenObjects['TestingTypeInfo'], - Viewer: NexusGenObjects['Viewer'], Wizard: NexusGenObjects['Wizard'], WizardBundler: NexusGenObjects['WizardBundler'], WizardFrontendFramework: NexusGenObjects['WizardFrontendFramework'], @@ -37,24 +46,31 @@ export const testUnionType = unionType({ t.members( 'App', 'Browser', - 'DashboardProject', - 'LocalProject', + 'CloudOrganization', + 'CloudOrganizationConnection', + 'CloudOrganizationEdge', + 'CloudProject', + 'CloudProjectConnection', + 'CloudProjectEdge', + 'CloudRecordKey', + 'CloudRun', + 'CloudRunCommitInfo', + 'CloudRunConnection', + 'CloudRunEdge', + 'CloudUser', 'Mutation', 'NavigationItem', 'NavigationMenu', + 'PageInfo', 'Project', 'Query', 'ResolvedBooleanOption', 'ResolvedConfig', 'ResolvedJsonOption', 'ResolvedNumberOption', - 'ResolvedOptionBase', 'ResolvedStringListOption', 'ResolvedStringOption', - 'RunCommit', - 'RunGroup', 'TestingTypeInfo', - 'Viewer', 'Wizard', 'WizardBundler', 'WizardFrontendFramework', diff --git a/packages/graphql/src/util/getOperation.ts b/packages/graphql/src/util/getOperation.ts new file mode 100644 index 000000000000..7c9792e23c36 --- /dev/null +++ b/packages/graphql/src/util/getOperation.ts @@ -0,0 +1,7 @@ +export function getOperation (req: Request) { + // +} + +export function getRootValue (req: Request) { + // +} diff --git a/packages/graphql/src/util/index.ts b/packages/graphql/src/util/index.ts index b866f6903bdb..f2ec7b67099c 100644 --- a/packages/graphql/src/util/index.ts +++ b/packages/graphql/src/util/index.ts @@ -1,4 +1,6 @@ /* eslint-disable padding-line-between-statements */ // created by autobarrel, do not modify directly +export * from './getOperation' +export * from './relayConnectionUtils' export * from './wizardGetConfigCode' diff --git a/packages/graphql/src/util/relayConnectionUtils.ts b/packages/graphql/src/util/relayConnectionUtils.ts new file mode 100644 index 000000000000..75de9bdd644d --- /dev/null +++ b/packages/graphql/src/util/relayConnectionUtils.ts @@ -0,0 +1,54 @@ +import { GraphQLError } from 'graphql' +import type { NexusGenAbstractTypeMembers } from '../gen/nxs.gen' + +class PublicError extends GraphQLError {} + +export function base64Encode (str: string): string { + if (typeof Buffer !== 'undefined') { + return Buffer.from(str).toString('base64') + } + + return btoa(str) +} + +export function base64Decode (str: string): string { + if (typeof Buffer !== 'undefined') { + return Buffer.from(str, 'base64').toString('utf-8') + } + + return atob(str) +} + +export function decodeNodeId ( + id: string, + allowedTypes?: T | T[], +): { type: NexusGenAbstractTypeMembers['Node'], value: string } { + const decoded = base64Decode(id) + const [type, value] = decoded.split(':') as [string, string] + + function isAllowedType (val: string): val is T { + if (!allowedTypes) { + return true + } + + return Array.isArray(allowedTypes) + ? allowedTypes.includes(type as T) + : type === allowedTypes + } + + if (isAllowedType(type)) { + return { type, value } + } + + throw new PublicError(`Invalid ID, expected one of ${allowedTypes}`) +} + +export function decodeNodeIds ( + ids: string[], + allowedTypes: T | T[], +): { type: T, value: string }[] { + return ids.map((id) => decodeNodeId(id, allowedTypes)) as { + type: T + value: string + }[] +} diff --git a/packages/graphql/test/integration/TestActions.ts b/packages/graphql/test/integration/TestActions.ts new file mode 100644 index 000000000000..eb627f6c8f51 --- /dev/null +++ b/packages/graphql/test/integration/TestActions.ts @@ -0,0 +1,66 @@ +import type { FoundBrowser } from '@packages/launcher' +import { BaseActions, BaseContext, Project } from '../../src' + +export class TestActions extends BaseActions { + ctx: BaseContext + + constructor (_ctx: BaseContext) { + super(_ctx) + this.ctx = _ctx + } + + installDependencies () {} + createConfigFile () {} + + initializeOpenProject () { + return null + } + + resolveOpenProjectConfig () { + return null + } + + addProject (projectRoot: string) { + return new Project(projectRoot, this.ctx) + } + + async launchOpenProject () {} + + async authenticate () { + this.ctx.setAuthenticatedUser({ + authToken: 'test-auth-token', + email: 'test@cypress.io', + name: 'cypress test', + }) + } + + async logout () { + this.ctx.setAuthenticatedUser(null) + } + + async getProjectId () { + return 'test-project-id' + } + async getRuns () { + return [] + } + async getRecordKeys () { + return [] + } + + async getBrowsers () { + const browser: FoundBrowser = { + displayName: 'chrome', + family: 'chromium', + majorVersion: '1.0.0', + name: 'chrome', + channel: 'dev', + version: '1.0.0', + path: '/dev/chrome', + } + + return [browser] + } + + async initializeConfig () {} +} diff --git a/packages/graphql/test/integration/TestContext.ts b/packages/graphql/test/integration/TestContext.ts new file mode 100644 index 000000000000..7bb588c1c966 --- /dev/null +++ b/packages/graphql/test/integration/TestContext.ts @@ -0,0 +1,40 @@ +import { LaunchArgs, OpenProjectLaunchOptions } from '@packages/types' +import { BaseActions, BaseContext } from '../../src' +import { Project, Wizard } from '../../src/entities' +import { remoteSchema } from '../../src/stitching/remoteSchema' +import { TestActions } from './TestActions' + +interface TestContextInjectionOptions { + wizard?: Wizard + launchArgs?: LaunchArgs + launchOptions?: OpenProjectLaunchOptions + Actions?: typeof TestActions +} + +export class TestContext extends BaseContext { + _remoteSchema = remoteSchema + + localProjects: Project[] = [] + readonly actions: BaseActions + viewer = null + + constructor ({ wizard, launchArgs, launchOptions, Actions }: TestContextInjectionOptions = {}) { + super(launchArgs || { + config: {}, + cwd: '/current/working/dir', + _: ['/current/working/dir'], + projectRoot: '/project/root', + invokedFromCli: false, + browser: null, + global: false, + testingType: 'e2e', + project: '/project/root', + os: 'linux', + }, launchOptions || {}) + + this.actions = Actions ? new Actions(this) : new TestActions(this) + if (wizard) { + this.wizard = wizard + } + } +} diff --git a/packages/graphql/test/integration/remoteSchemaTest.ts b/packages/graphql/test/integration/remoteSchemaTest.ts new file mode 100644 index 000000000000..cea8a446e9de --- /dev/null +++ b/packages/graphql/test/integration/remoteSchemaTest.ts @@ -0,0 +1,3 @@ +import { remoteSchema } from '../../src/stitching/remoteSchema' + +export const remoteSchemaTest = remoteSchema diff --git a/packages/graphql/test/integration/utils.ts b/packages/graphql/test/integration/utils.ts index 860b9c7c9813..17928c9f5b35 100644 --- a/packages/graphql/test/integration/utils.ts +++ b/packages/graphql/test/integration/utils.ts @@ -1,112 +1,10 @@ import axios from 'axios' -import type { FoundBrowser, FullConfig, LaunchArgs, OpenProjectLaunchOptions } from '@packages/types' -import { BaseActions, BaseContext, DashboardProject, LocalProject, Viewer, Wizard } from '../../src' +import { BaseContext } from '../../src' import { startGraphQLServer, closeGraphQLServer, setServerContext } from '../../src/server' -interface TestContextInjectionOptions { - wizard?: Wizard - launchArgs?: LaunchArgs - launchOptions?: OpenProjectLaunchOptions - Actions?: typeof TestActions -} - -export class TestActions extends BaseActions { - ctx: BaseContext - - constructor (_ctx: BaseContext, private cfg?: FullConfig) { - super(_ctx) - this.ctx = _ctx - } - - installDependencies () {} - createConfigFile () {} - - async launchOpenProject () {} - resolveOpenProjectConfig (): FullConfig { - if (this.cfg) { - return this.cfg - } - - return { - projectRoot: '/root/path', - resolved: {}, - } - } +export { TestActions } from './TestActions' - addProject (projectRoot: string) { - return new LocalProject(projectRoot, this.ctx) - } - - async authenticate () { - this.ctx.viewer = new Viewer(this.ctx, { - authToken: 'test-auth-token', - email: 'test@cypress.io', - name: 'cypress test', - }) - } - - async logout () { - this.ctx.viewer = null - } - - async getProjectId () { - return 'test-project-id' - } - async getRuns () { - return [] - } - async getRecordKeys () { - return [] - } - - async initializeOpenProject () {} - - async getBrowsers () { - const browser: FoundBrowser = { - displayName: 'chrome', - family: 'chromium', - majorVersion: '1.0.0', - name: 'chrome', - channel: 'dev', - version: '1.0.0', - path: '/dev/chrome', - } - - return [browser] - } - - isFirstTime (projectRoot: string, testingType: Cypress.TestingType) { - return false - } - - async initializeConfig () {} -} - -export class TestContext extends BaseContext { - localProjects: LocalProject[] = [] - dashboardProjects: DashboardProject[] = [] - readonly actions: BaseActions - viewer = null - - constructor ({ wizard, launchArgs, launchOptions, Actions }: TestContextInjectionOptions = {}) { - super(launchArgs || { - config: {}, - cwd: '/current/working/dir', - _: ['/current/working/dir'], - projectRoot: '/project/root', - invokedFromCli: false, - browser: null, - testingType: 'e2e', - project: '/project/root', - os: 'linux', - }, launchOptions || {}) - - this.actions = Actions ? new Actions(this) : new TestActions(this) - if (wizard) { - this.wizard = wizard - } - } -} +export { TestContext } from './TestContext' /** * Creates a new GraphQL server to query during integration tests. diff --git a/packages/graphql/test/unit/entities/Wizard.spec.ts b/packages/graphql/test/unit/entities/Wizard.spec.ts index 7cb8803c2e43..8f183cc6b947 100644 --- a/packages/graphql/test/unit/entities/Wizard.spec.ts +++ b/packages/graphql/test/unit/entities/Wizard.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai' -import { LocalProject, Wizard } from '../../../src' +import { Project, Wizard } from '../../../src' import { TestActions, TestContext } from '../../integration/utils' const createActionsWithResolvedConfig = () => { @@ -18,7 +18,7 @@ describe('Wizard', () => { it('progresses through wizard steps', () => { const Actions = createActionsWithResolvedConfig() const ctx = new TestContext({ Actions }) - const project = new LocalProject('/', ctx) + const project = new Project('/', ctx) ctx.localProjects = [project] const wizard = new Wizard(ctx) diff --git a/packages/graphql/tsconfig.json b/packages/graphql/tsconfig.json index b14a13880271..9419cf3c4b0e 100644 --- a/packages/graphql/tsconfig.json +++ b/packages/graphql/tsconfig.json @@ -11,7 +11,9 @@ "compilerOptions": { "importHelpers": true, "strict": true, + "allowJs": false, "noImplicitAny": true, + "resolveJsonModule": true, "experimentalDecorators": true, "noUncheckedIndexedAccess": true, "importsNotUsedAsValues": "error", diff --git a/packages/launchpad/.eslintrc.json b/packages/launchpad/.eslintrc.json index e2a2e35a7640..1bc9f51f6d82 100644 --- a/packages/launchpad/.eslintrc.json +++ b/packages/launchpad/.eslintrc.json @@ -13,6 +13,19 @@ "cypress/globals": true }, "overrides": [ + { + "files": "**/*.vue", + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "@packages/graphql/*" + ] + } + ] + } + }, { "files": [ "lib/*" diff --git a/packages/launchpad/cypress.json b/packages/launchpad/cypress.json index 758704df6fe5..b63f41bb9d99 100644 --- a/packages/launchpad/cypress.json +++ b/packages/launchpad/cypress.json @@ -1,5 +1,5 @@ { - "projectId": "ypt4pf", + "projectId": "sehy69", "baseUrl": "http://localhost:5555", "viewportWidth": 800, "viewportHeight": 850, diff --git a/packages/launchpad/cypress/fixtures/browsers/long-browsers-list.ts b/packages/launchpad/cypress/fixtures/browsers/long-browsers-list.ts index 6c2faf845ae1..cf9a11380b87 100644 --- a/packages/launchpad/cypress/fixtures/browsers/long-browsers-list.ts +++ b/packages/launchpad/cypress/fixtures/browsers/long-browsers-list.ts @@ -1,4 +1,14 @@ export const longBrowsersList = [ + { + "name": "electron", + "displayName": "Electron", + "family": "chromium", + "channel": "stable", + "version": "73.0.3683.121", + "path": "", + "majorVersion": "73", + "info": "Info about electron browser" + }, { "name": "chrome", "displayName": "Chrome", @@ -71,16 +81,6 @@ export const longBrowsersList = [ "path": "/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev", "majorVersion": "79" }, - { - "name": "electron", - "displayName": "Electron", - "family": "chromium", - "channel": "stable", - "version": "73.0.3683.121", - "path": "", - "majorVersion": "73", - "info": "Info about electron browser" - }, { "name": "firefox", "displayName": "Firefox", diff --git a/packages/launchpad/cypress/plugins/index.js b/packages/launchpad/cypress/plugins/index.js index 248fccdcdf82..a4615f17809d 100644 --- a/packages/launchpad/cypress/plugins/index.js +++ b/packages/launchpad/cypress/plugins/index.js @@ -31,7 +31,7 @@ module.exports = (on, config) => { viteConfig: { // TODO(tim): Figure out why this isn't being picked up optimizeDeps: { - include: ['@headlessui/vue'], + include: ['@headlessui/vue', 'vue-prism-component'], }, }, }) diff --git a/packages/launchpad/package.json b/packages/launchpad/package.json index b125c6cb129f..a1b1486800ad 100644 --- a/packages/launchpad/package.json +++ b/packages/launchpad/package.json @@ -3,7 +3,7 @@ "version": "0.0.0-development", "private": true, "scripts": { - "types": "vue-tsc --noEmit", + "check-ts": "vue-tsc --noEmit", "build-prod": "cross-env NODE_ENV=production vite build", "clean": "rm -rf dist && rm -rf ./node_modules/.vite && echo 'cleaned'", "clean-deps": "rm -rf node_modules", @@ -24,8 +24,7 @@ "@iconify/vue": "3.0.0-beta.1", "@intlify/vite-plugin-vue-i18n": "2.4.0", "@testing-library/cypress": "8.0.0", - "@urql/core": "2.1.5", - "@urql/exchange-execute": "^1.0.4", + "@urql/core": "2.3.1", "@urql/vue": "0.4.3", "@vitejs/plugin-vue": "1.2.4", "@vitejs/plugin-vue-jsx": "1.1.6", diff --git a/packages/launchpad/src/App.vue b/packages/launchpad/src/App.vue index cecd15d52be1..2203f7f7a861 100644 --- a/packages/launchpad/src/App.vue +++ b/packages/launchpad/src/App.vue @@ -8,7 +8,7 @@ diff --git a/packages/launchpad/src/runs/RunCard.spec.tsx b/packages/launchpad/src/runs/RunCard.spec.tsx index 50e51dc1ba08..b800a87a5684 100644 --- a/packages/launchpad/src/runs/RunCard.spec.tsx +++ b/packages/launchpad/src/runs/RunCard.spec.tsx @@ -1,30 +1,11 @@ -import { RunGroup } from '../../../graphql/src/entities/run' -import { RunFragmentDoc } from '../generated/graphql' +import { RunCardFragmentDoc } from '../generated/graphql-test' import RunCard from './RunCard.vue' describe('', { viewportHeight: 400 }, () => { it('playground', () => { - cy.mountFragment(RunFragmentDoc, { + cy.mountFragment(RunCardFragmentDoc, { type: (ctx) => { - return new RunGroup({ - createdAt: new Date().toString(), - completedAt: new Date().toString(), - status: 'passed', - id: '1', - commit: { - message: 'Updating the hover state for the button component', - authorName: 'Ryan', - authorEmail: 'ryan@cypress.io', - branch: 'develop', - sha: 'shashasha', - url: 'https://github.com', - }, - totalDuration: 1000, - totalPassed: 5, - totalFailed: 0, - totalSkipped: 0, - totalPending: 4, - }) + return ctx.stubData.CloudRunStubs.allPassing }, render: (gqlVal) => { return ( diff --git a/packages/launchpad/src/runs/RunCard.vue b/packages/launchpad/src/runs/RunCard.vue index 57997ff3cc2d..b053a27e1144 100644 --- a/packages/launchpad/src/runs/RunCard.vue +++ b/packages/launchpad/src/runs/RunCard.vue @@ -1,10 +1,10 @@ @@ -26,20 +26,16 @@ import IconUserCircle from 'virtual:vite-icons/bx/bx-user-circle' // carbon:branch import IconBranch from 'virtual:vite-icons/carbon/branch' import { gql } from '@urql/core' -import type { RunFragment } from '../generated/graphql' +import type { RunCardFragment } from '../generated/graphql' import { computed } from 'vue-demi' -import type { RunGroupTotals } from '@packages/graphql/src/entities/run' gql` -fragment Run on RunGroup { +fragment RunCard on CloudRun { + id createdAt - totalPassed - totalFailed - totalPending - totalSkipped - totalDuration - status - commit { + ...RunIcon + ...RunResults + commitInfo { authorName authorEmail message @@ -49,28 +45,21 @@ fragment Run on RunGroup { ` const props = defineProps<{ - gql: RunFragment + gql: RunCardFragment }>() const run = computed(() => props.gql) const runInfo = [{ - text: run.value.commit.authorName || '', + text: run.value.commitInfo?.authorName, icon: IconUserCircle }, { - text: run.value.commit.branch || '', + text: run.value.commitInfo?.branch, icon: IconBranch }, { - text: new Date(run.value.createdAt).toLocaleTimeString() -}] + text: run.value.createdAt ? new Date(run.value.createdAt).toLocaleTimeString() : null +}].filter(o => Boolean(o.text)) -const runGroupTotals: RunGroupTotals = { - totalPassed: props.gql.totalPassed || 0, - totalFailed: props.gql.totalFailed || 0, - totalPending: props.gql.totalPending || 0, - totalSkipped: props.gql.totalSkipped || 0, - totalDuration: props.gql.totalDuration || 0, -} \ No newline at end of file diff --git a/packages/launchpad/src/runs/RunIcon.spec.tsx b/packages/launchpad/src/runs/RunIcon.spec.tsx index b021d928a09b..f8fc782ec0ba 100644 --- a/packages/launchpad/src/runs/RunIcon.spec.tsx +++ b/packages/launchpad/src/runs/RunIcon.spec.tsx @@ -1,15 +1,22 @@ +import { RunIconFragmentDoc } from '../generated/graphql-test' import RunIcon from './RunIcon.vue' describe('', { viewportWidth: 80, viewportHeight: 200 }, () => { it('playground', () => { - cy.mount(() => ( -
- -
- -
- -
- )) + cy.mountFragmentList(RunIconFragmentDoc, { + type: (ctx) => { + return Object.values(ctx.stubData.CloudRunStubs) + }, + render: (gqlList) => ( +
+ {gqlList.map((gql) => ( + <> + +
+ + ))} +
+ ), + }) }) }) diff --git a/packages/launchpad/src/runs/RunIcon.vue b/packages/launchpad/src/runs/RunIcon.vue index c37fec715e15..fd376c524437 100644 --- a/packages/launchpad/src/runs/RunIcon.vue +++ b/packages/launchpad/src/runs/RunIcon.vue @@ -1,23 +1,29 @@ \ No newline at end of file diff --git a/packages/launchpad/src/runs/RunResults.spec.tsx b/packages/launchpad/src/runs/RunResults.spec.tsx index b9e86181c922..3ba5a2fe9b06 100644 --- a/packages/launchpad/src/runs/RunResults.spec.tsx +++ b/packages/launchpad/src/runs/RunResults.spec.tsx @@ -1,16 +1,22 @@ -import type { RunGroupTotals } from '@packages/graphql/src/entities/run/Run' import RunResults from './RunResults.vue' - -const totals: RunGroupTotals = { - totalDuration: 1000, - totalPassed: 5, - totalFailed: 0, - totalSkipped: 0, - totalPending: 4, -} +import { RunCardFragmentDoc } from '../generated/graphql-test' describe('', () => { it('playground', () => { - cy.mount(() => ) + cy.mountFragment(RunCardFragmentDoc, { + type: (ctx) => ({ + // TODO: move this into a test mock layer + __typename: 'CloudRun' as const, + id: 'CloudRun:1', + totalDuration: 1000, + totalPassed: 5, + totalFailed: 0, + totalSkipped: 0, + totalPending: 4, + }), + render (gql) { + return + }, + }) }) }) diff --git a/packages/launchpad/src/runs/RunResults.vue b/packages/launchpad/src/runs/RunResults.vue index 3e9c7aa0f554..1f24cf23eeda 100644 --- a/packages/launchpad/src/runs/RunResults.vue +++ b/packages/launchpad/src/runs/RunResults.vue @@ -10,22 +10,23 @@
- {{props.totals.totalSkipped}} + {{props.gql.totalSkipped}}
- {{props.totals.totalPassed}} + {{props.gql.totalPassed}}
- {{props.totals.totalFailed}} + {{props.gql.totalFailed}}
\ No newline at end of file diff --git a/packages/launchpad/src/runs/RunsPage.spec.tsx b/packages/launchpad/src/runs/RunsPage.spec.tsx index bc2ddeda6975..ef32857792c1 100644 --- a/packages/launchpad/src/runs/RunsPage.spec.tsx +++ b/packages/launchpad/src/runs/RunsPage.spec.tsx @@ -1,9 +1,15 @@ +import { RunsPageFragmentDoc } from '../generated/graphql-test' import RunsPage from './RunsPage.vue' describe('', () => { it('playground', () => { - cy.mount(() => ( - - )) + cy.mountFragment(RunsPageFragmentDoc, { + type: (ctx) => { + return ctx.stubData.CloudProjectStubs.componentProject + }, + render: (gql) => ( + + ), + }) }) }) diff --git a/packages/launchpad/src/runs/RunsPage.vue b/packages/launchpad/src/runs/RunsPage.vue index e6191c818de6..3d0cf5de104b 100644 --- a/packages/launchpad/src/runs/RunsPage.vue +++ b/packages/launchpad/src/runs/RunsPage.vue @@ -1,50 +1,31 @@ \ No newline at end of file diff --git a/packages/launchpad/src/settings/project/ProjectId.spec.tsx b/packages/launchpad/src/settings/project/ProjectId.spec.tsx index 6469565b363c..db4984ef13a0 100644 --- a/packages/launchpad/src/settings/project/ProjectId.spec.tsx +++ b/packages/launchpad/src/settings/project/ProjectId.spec.tsx @@ -1,5 +1,5 @@ import { computed, ref } from 'vue' -import { ProjectIdFragmentDoc } from '../../generated/graphql' +import { ProjectIdFragmentDoc } from '../../generated/graphql-test' import ProjectId from './ProjectId.vue' describe('', () => { diff --git a/packages/launchpad/src/settings/project/ProjectId.vue b/packages/launchpad/src/settings/project/ProjectId.vue index 4a16de8e1aa0..0edfbbcd7acb 100644 --- a/packages/launchpad/src/settings/project/ProjectId.vue +++ b/packages/launchpad/src/settings/project/ProjectId.vue @@ -39,7 +39,7 @@ import { useI18n } from '../../composables' import type { ProjectIdFragment } from '../../generated/graphql' gql` -fragment ProjectId on LocalProject { +fragment ProjectId on Project { projectId } ` diff --git a/packages/launchpad/src/settings/project/ProjectSettings.vue b/packages/launchpad/src/settings/project/ProjectSettings.vue index 351e4b1be375..5dad54553dd6 100644 --- a/packages/launchpad/src/settings/project/ProjectSettings.vue +++ b/packages/launchpad/src/settings/project/ProjectSettings.vue @@ -1,15 +1,14 @@