diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index fd0647ddc691..686e662911a3 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -68,7 +68,7 @@ windowsWorkflowFilters: &windows-workflow-filters or: - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - - equal: [ 'update-v8-snapshot-cache-on-develop', 'fix-duplicate-and-expired-cookies', << pipeline.git.branch >> ] + - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -134,7 +134,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "astone123/ventura-webpack-permission-testing" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 8c9b11482dac..9a237df1f8b7 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,11 +1,12 @@ -## 12.6.1 +## 12.7.0 _Released 03/1/2023 (PENDING)_ **Features:** - It is now possible to set `hostOnly` cookies with [`cy.setCookie()`](https://docs.cypress.io/api/commands/setcookie) for a given domain. Addresses [#16856](https://github.com/cypress-io/cypress/issues/16856) and [#17527](https://github.com/cypress-io/cypress/issues/17527). +- Added a Public API for third party component libraries to define a Framework Definition, embedding their library into the Cypress onboarding workflow. Learn more [here](https://docs.cypress.io/guides/component-testing/third-party-definitions). Implemented in [#25780](https://github.com/cypress-io/cypress/pull/25780) and closes [#25638](https://github.com/cypress-io/cypress/issues/25638). **Bugfixes:** diff --git a/cli/index.mjs b/cli/index.mjs index 7b616f65a344..dcf09178f398 100644 --- a/cli/index.mjs +++ b/cli/index.mjs @@ -8,6 +8,8 @@ export default cypress export const defineConfig = cypress.defineConfig +export const defineComponentFramework = cypress.defineComponentFramework + export const run = cypress.run export const open = cypress.open diff --git a/cli/lib/cypress.js b/cli/lib/cypress.js index 02348266aa54..ff140859ff22 100644 --- a/cli/lib/cypress.js +++ b/cli/lib/cypress.js @@ -87,6 +87,24 @@ const cypressModuleApi = { defineConfig (config) { return config }, + + /** + * Provides automatic code completion for Component Frameworks Definitions. + * While it's not strictly necessary for Cypress to parse your configuration, we + * recommend wrapping your Component Framework Definition object with `defineComponentFramework()` + * @example + * module.exports = defineComponentFramework({ + * type: 'cypress-ct-solid-js' + * // ... + * }) + * + * @see ../types/cypress-npm-api.d.ts + * @param {Cypress.ThirdPartyComponentFrameworkDefinition} config + * @returns {Cypress.ThirdPartyComponentFrameworkDefinition} the configuration passed in parameter + */ + defineComponentFramework (config) { + return config + }, } module.exports = cypressModuleApi diff --git a/cli/types/cypress-npm-api.d.ts b/cli/types/cypress-npm-api.d.ts index 23cb8d4e3f13..41a38abfdacd 100644 --- a/cli/types/cypress-npm-api.d.ts +++ b/cli/types/cypress-npm-api.d.ts @@ -397,6 +397,21 @@ declare module 'cypress' { * @returns {Cypress.ConfigOptions} the configuration passed in parameter */ defineConfig(config: Cypress.ConfigOptions): Cypress.ConfigOptions + + /** + * Provides automatic code completion for Component Frameworks Definitions. + * While it's not strictly necessary for Cypress to parse your configuration, we + * recommend wrapping your Component Framework Definition object with `defineComponentFramework()` + * @example + * module.exports = defineComponentFramework({ + * type: 'cypress-ct-solid-js' + * }) + * + * @see ../types/cypress-npm-api.d.ts + * @param {Cypress.ThirdPartyComponentFrameworkDefinition} config + * @returns {Cypress.ThirdPartyComponentFrameworkDefinition} the configuration passed in parameter + */ + defineComponentFramework(config: Cypress.ThirdPartyComponentFrameworkDefinition): Cypress.ThirdPartyComponentFrameworkDefinition } // export Cypress NPM module interface diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index fb927383598c..d381933ace47 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -3283,6 +3283,179 @@ declare namespace Cypress { type PickConfigOpt = T extends keyof DefineDevServerConfig ? DefineDevServerConfig[T] : any + interface DependencyToInstall { + dependency: CypressComponentDependency + satisfied: boolean + loc: string | null + detectedVersion: string | null + } + + interface CypressComponentDependency { + /** + * Unique idenitifer. + * @example 'reactscripts' + */ + type: string + + /** + * Name to display in the user interface. + * @example "React Scripts" + */ + name: string + + /** + * Package name on npm. + * @example react-scripts + */ + package: string + + /** + * Code to run when installing. Version is optional. + * + * Should be @. + * + * @example `react` + * @example `react@18` + * @example `react-scripts` + */ + installer: string + + /** + * Description shown in UI. It is recommended to use the same one the package uses on npm. + * @example 'Create React apps with no build configuration' + */ + description: string + + /** + * Minimum version supported. Should conform to Semantic Versioning as used in `package.json`. + * @see https://docs.npmjs.com/cli/v9/configuring-npm/package-json#dependencies + * @example '^=4.0.0 || ^=5.0.0' + * @example '^2.0.0' + */ + minVersion: string + } + + interface ResolvedComponentFrameworkDefinition { + /** + * A semantic, unique identifier. + * Must begin with `cypress-ct-` or `@org/cypress-ct-` for third party implementations. + * @example 'reactscripts' + * @example 'nextjs' + * @example 'cypress-ct-solid-js' + */ + type: string + + /** + * Used as the flag for `getPreset` for meta framworks, such as finding the webpack config for CRA, Angular, etc. + * It is also the name of the string added to `cypress.config` + * + * @example + * export default { + * component: { + * devServer: { + * framework: 'create-react-app' // can be 'next', 'create-react-app', etc etc. + * } + * } + * } + */ + configFramework: string + + /** + * Library (React, Vue) or template (aka "meta framework") (CRA, Next.js, Angular) + */ + category: 'library' | 'template' + + /** + * Name displayed in Launchpad when doing initial setup. + * @example 'Solid.js' + * @example 'Create React App' + */ + name: string + + /** + * Supported bundlers. + */ + supportedBundlers: Array<'webpack' | 'vite'> + + /** + * Used to attempt to automatically select the correct framework/bundler from the dropdown. + * + * @example + * const SOLID_DETECTOR: Dependency = { + * type: 'solid', + * name: 'Solid.js', + * package: 'solid-js', + * installer: 'solid-js', + * description: 'Solid is a declarative JavaScript library for creating user interfaces', + * minVersion: '^1.0.0', + * } + */ + detectors: CypressComponentDependency[] + + /** + * Array of required dependencies. This could be the bundler and JavaScript library. + */ + dependencies: (bundler: 'webpack' | 'vite', projectPath: string) => Promise + + /** + * This is used interally by Cypress for the "Create From Component" feature. + */ + codeGenFramework?: 'react' | 'vue' | 'svelte' | 'angular' + + /** + * This is used interally by Cypress for the "Create From Component" feature. + * @example '*.{js,jsx,tsx}' + */ + glob?: string + + /** + * This is the path to get mount, eg `import { mount } from , + * @example: `cypress-ct-solidjs/src/mount` + */ + mountModule: (projectPath: string) => Promise + + /** + * Support status. Internally alpha | beta | full. + * Community integrations are "community". + */ + supportStatus: 'alpha' | 'beta' | 'full' | 'community' + + /** + * Function returning string for used for the component-index.html file. + * Cypress provides a default if one isn't specified for third party integrations. + */ + componentIndexHtml?: () => string + + /** + * Used for the Create From Comopnent feature. + * This is currently not supported for third party frameworks. + */ + specPattern?: '**/*.cy.ts' + } + + type ComponentFrameworkDefinition = Omit & { + dependencies: (bundler: 'webpack' | 'vite') => CypressComponentDependency[] + } + + /** + * Certain properties are not supported for third party frameworks right now, + * such as ones related to the "Create From" feature. This is a subset of + * properties that are exposed for public usage. + */ + + type ThirdPartyComponentFrameworkDefinition = Pick & { + /** + * @example `cypress-ct-${string} for third parties. Any string is valid internally. + */ + type: string + + /** + * Raw SVG icon that will be displayed in the Project Setup Wizard. Used for third parties that + * want to render a custom icon. + */ + icon?: string + } + interface AngularDevServerProjectConfig { root: string sourceRoot: string diff --git a/cli/types/tests/cypress-npm-api-test.ts b/cli/types/tests/cypress-npm-api-test.ts index 3a0feebd7df6..ba04ed7a5802 100644 --- a/cli/types/tests/cypress-npm-api-test.ts +++ b/cli/types/tests/cypress-npm-api-test.ts @@ -1,6 +1,6 @@ // type tests for Cypress NPM module // https://on.cypress.io/module-api -import cypress, { defineConfig } from 'cypress' +import cypress, { defineComponentFramework, defineConfig } from 'cypress' cypress.run // $ExpectType (options?: Partial | undefined) => Promise cypress.open // $ExpectType (options?: Partial | undefined) => Promise @@ -55,6 +55,32 @@ const config = defineConfig({ modifyObstructiveCode: true }) +const solid = { + type: 'solid-js', + name: 'Solid.js', + package: 'solid-js', + installer: 'solid-js', + description: 'Solid is a declarative JavaScript library for creating user interfaces', + minVersion: '^1.0.0' +} + +const thirdPartyFrameworkDefinition = defineComponentFramework({ + type: 'cypress-ct-third-party', + name: 'Third Party', + dependencies: (bundler) => [solid], + detectors: [solid], + supportedBundlers: ['vite', 'webpack'], + icon: '...' +}) + +const thirdPartyFrameworkDefinitionInvalidStrings = defineComponentFramework({ + type: 'cypress-ct-third-party', + name: 'Third Party', + dependencies: (bundler) => [], + detectors: [{}], // $ExpectError + supportedBundlers: ['metro', 'webpack'] // $ExpectError +}) + // component options const componentConfigNextWebpack: Cypress.ConfigOptions = { component: { diff --git a/npm/webpack-dev-server/src/devServer.ts b/npm/webpack-dev-server/src/devServer.ts index e7cabad50c17..675328144e22 100644 --- a/npm/webpack-dev-server/src/devServer.ts +++ b/npm/webpack-dev-server/src/devServer.ts @@ -114,7 +114,25 @@ export type PresetHandlerResult = { frameworkConfig: Configuration, sourceWebpac type Optional = Pick, K> & Omit +const thirdPartyDefinitionPrefixes = { + // matches @org/cypress-ct-* + namespacedPrefixRe: /^@.+?\/cypress-ct-.+/, + globalPrefix: 'cypress-ct-', +} + +export function isThirdPartyDefinition (framework: string) { + return framework.startsWith(thirdPartyDefinitionPrefixes.globalPrefix) || + thirdPartyDefinitionPrefixes.namespacedPrefixRe.test(framework) +} + async function getPreset (devServerConfig: WebpackDevServerConfig): Promise> { + const defaultWebpackModules = () => ({ sourceWebpackModulesResult: sourceDefaultWebpackDependencies(devServerConfig) }) + + // Third party library (eg solid-js, lit, etc) + if (devServerConfig.framework && isThirdPartyDefinition(devServerConfig.framework)) { + return defaultWebpackModules() + } + switch (devServerConfig.framework) { case 'create-react-app': return createReactAppHandler(devServerConfig) @@ -134,7 +152,7 @@ async function getPreset (devServerConfig: WebpackDevServerConfig): Promise framework.configFramework === config?.component?.devServer.framework) + return this.ctx.coreData.wizard.frameworks.find((framework) => framework.configFramework === config?.component?.devServer.framework) } } diff --git a/packages/data-context/src/actions/WizardActions.ts b/packages/data-context/src/actions/WizardActions.ts index 4436a865b7fa..e8f8ab2cd96d 100644 --- a/packages/data-context/src/actions/WizardActions.ts +++ b/packages/data-context/src/actions/WizardActions.ts @@ -1,5 +1,5 @@ import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen' -import { detectFramework, WIZARD_FRAMEWORKS, commandsFileBody, supportFileComponent, supportFileE2E, WizardBundler, WizardFrontendFramework } from '@packages/scaffold-config' +import { detectFramework, commandsFileBody, supportFileComponent, supportFileE2E, getBundler, CT_FRAMEWORKS, resolveComponentFrameworkDefinition, detectThirdPartyCTFrameworks } from '@packages/scaffold-config' import assert from 'assert' import path from 'path' import Debug from 'debug' @@ -9,6 +9,7 @@ const debug = Debug('cypress:data-context:wizard-actions') import type { DataContext } from '..' import { addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions } from '@packages/config' +import componentIndexHtmlGenerator from '@packages/scaffold-config/src/component-index-template' export class WizardActions { constructor (private ctx: DataContext) {} @@ -23,14 +24,14 @@ export class WizardActions { return this.ctx.wizardData } - setFramework (framework: WizardFrontendFramework | null): void { - const next = WIZARD_FRAMEWORKS.find((x) => x.type === framework?.type) + setFramework (framework: Cypress.ResolvedComponentFrameworkDefinition | null): void { + const next = this.ctx.coreData.wizard.frameworks.find((x) => x.type === framework?.type) this.ctx.update((coreData) => { coreData.wizard.chosenFramework = framework }) - if (next?.supportedBundlers?.length === 1) { + if (next?.supportedBundlers?.[0] && next.supportedBundlers.length === 1) { this.setBundler(next?.supportedBundlers?.[0]) return @@ -41,7 +42,7 @@ export class WizardActions { // if the previous bundler was incompatible with the // new framework that was selected, we need to reset it const doesNotSupportChosenBundler = (chosenBundler && !new Set( - this.ctx.coreData.wizard.chosenFramework?.supportedBundlers.map((x) => x.type) || [], + this.ctx.coreData.wizard.chosenFramework?.supportedBundlers ?? [], ).has(chosenBundler.type)) ?? false const prevFramework = this.ctx.coreData.wizard.chosenFramework?.type ?? null @@ -51,9 +52,21 @@ export class WizardActions { } } - setBundler (bundler: WizardBundler | null) { + getNullableBundler (bundler: 'vite' | 'webpack' | null) { + if (!bundler) { + return null + } + + try { + return getBundler(bundler) + } catch (e) { + return null + } + } + + setBundler (bundler: 'vite' | 'webpack' | null) { this.ctx.update((coreData) => { - coreData.wizard.chosenBundler = bundler + coreData.wizard.chosenBundler = this.getNullableBundler(bundler) }) return this.ctx.coreData.wizard @@ -87,7 +100,7 @@ export class WizardActions { this.resetWizard() - const detected = await detectFramework(this.ctx.currentProject) + const detected = await detectFramework(this.ctx.currentProject, this.ctx.coreData.wizard.frameworks) debug('detected %o', detected) @@ -100,8 +113,8 @@ export class WizardActions { return } - coreData.wizard.detectedBundler = detected.bundler || detected.framework.supportedBundlers[0] - coreData.wizard.chosenBundler = detected.bundler || detected.framework.supportedBundlers[0] + coreData.wizard.detectedBundler = this.getNullableBundler(detected.bundler || detected.framework.supportedBundlers[0]) + coreData.wizard.chosenBundler = this.getNullableBundler(detected.bundler || detected.framework.supportedBundlers[0]) }) } } @@ -148,6 +161,20 @@ export class WizardActions { } } + async detectFrameworks () { + if (!this.ctx.currentProject) { + return + } + + const officialFrameworks = CT_FRAMEWORKS.map((framework) => resolveComponentFrameworkDefinition(framework)) + const thirdParty = await detectThirdPartyCTFrameworks(this.ctx.currentProject) + const resolvedThirdPartyFrameworks = thirdParty.map(resolveComponentFrameworkDefinition) + + this.ctx.update((d) => { + d.wizard.frameworks = officialFrameworks.concat(resolvedThirdPartyFrameworks) + }) + } + private async scaffoldE2E () { // Order of the scaffoldedFiles is intentional, confirm before changing const scaffoldedFiles = await Promise.all([ @@ -295,14 +322,16 @@ export class WizardActions { } } - private async scaffoldComponentIndexHtml (chosenFramework: WizardFrontendFramework): Promise { + private async scaffoldComponentIndexHtml (chosenFramework: Cypress.ResolvedComponentFrameworkDefinition): Promise { const componentIndexHtmlPath = path.join(this.projectRoot, 'cypress', 'support', 'component-index.html') await this.ensureDir('support') + const defaultComponentIndex = componentIndexHtmlGenerator() + return this.scaffoldFile( componentIndexHtmlPath, - chosenFramework.componentIndexHtml(), + chosenFramework.componentIndexHtml?.() ?? defaultComponentIndex(), 'The HTML wrapper that each component is served with. Used for global fonts, CSS, JS, HTML, etc.', ) } diff --git a/packages/data-context/src/codegen/spec-options.ts b/packages/data-context/src/codegen/spec-options.ts index dea6618626ff..1f9cdac94fbc 100644 --- a/packages/data-context/src/codegen/spec-options.ts +++ b/packages/data-context/src/codegen/spec-options.ts @@ -1,6 +1,5 @@ import type { ParsedPath } from 'path' import type { CodeGenType } from '@packages/graphql/src/gen/nxs.gen' -import type { WizardFrontendFramework } from '@packages/scaffold-config' import fs from 'fs-extra' import { uniq, upperFirst } from 'lodash' import path from 'path' @@ -14,7 +13,7 @@ interface CodeGenOptions { isDefaultSpecPattern: boolean specPattern: string[] currentProject: string | null - framework?: WizardFrontendFramework + framework?: Cypress.ResolvedComponentFrameworkDefinition specs?: FoundSpec[] componentName?: string isDefault?: boolean diff --git a/packages/data-context/src/data/ProjectConfigManager.ts b/packages/data-context/src/data/ProjectConfigManager.ts index 30508f02ff49..39f205c52573 100644 --- a/packages/data-context/src/data/ProjectConfigManager.ts +++ b/packages/data-context/src/data/ProjectConfigManager.ts @@ -18,7 +18,7 @@ import { CypressEnv } from './CypressEnv' import { autoBindDebug } from '../util/autoBindDebug' import type { EventRegistrar } from './EventRegistrar' import type { DataContext } from '../DataContext' -import { DependencyToInstall, isDependencyInstalled, WIZARD_BUNDLERS, WIZARD_DEPENDENCIES, WIZARD_FRAMEWORKS } from '@packages/scaffold-config' +import { isDependencyInstalled, WIZARD_BUNDLERS } from '@packages/scaffold-config' const debug = debugLib(`cypress:lifecycle:ProjectConfigManager`) @@ -191,14 +191,20 @@ export class ProjectConfigManager { // Use a map since sometimes the same dependency can appear in `bundler` and `framework`, // for example webpack appears in both `bundler: 'webpack', framework: 'react-scripts'` - const unsupportedDeps = new Map() + const unsupportedDeps = new Map() if (!bundler) { return } - const isFrameworkSatisfied = async (bundler: typeof WIZARD_BUNDLERS[number], framework: typeof WIZARD_FRAMEWORKS[number]) => { - for (const dep of await (framework.dependencies(bundler.type, this.options.projectRoot))) { + const isFrameworkSatisfied = async (bundler: typeof WIZARD_BUNDLERS[number], framework: Cypress.ResolvedComponentFrameworkDefinition) => { + const deps = await framework.dependencies(bundler.type, this.options.projectRoot) + + debug('deps are %o', deps) + + for (const dep of deps) { + debug('detecting %s in %s', dep.dependency.name, this.options.projectRoot) + const res = await isDependencyInstalled(dep.dependency, this.options.projectRoot) if (!res.satisfied) { @@ -209,9 +215,9 @@ export class ProjectConfigManager { return true } - const frameworks = WIZARD_FRAMEWORKS.filter((x) => x.configFramework === devServerOptions.framework) + const frameworks = this.options.ctx.coreData.wizard.frameworks.filter((x) => x.configFramework === devServerOptions.framework) - const mismatchedFrameworkDeps = new Map() + const mismatchedFrameworkDeps = new Map() let isSatisfied = false diff --git a/packages/data-context/src/data/coreDataShape.ts b/packages/data-context/src/data/coreDataShape.ts index 8f6c66e85c42..3bed2cc4860d 100644 --- a/packages/data-context/src/data/coreDataShape.ts +++ b/packages/data-context/src/data/coreDataShape.ts @@ -1,5 +1,5 @@ import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep, BannerState } from '@packages/types' -import type { WizardFrontendFramework, WizardBundler } from '@packages/scaffold-config' +import { WizardBundler, CT_FRAMEWORKS, resolveComponentFrameworkDefinition } from '@packages/scaffold-config' import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen' import type { App, BrowserWindow } from 'electron' import type { ChildProcess } from 'child_process' @@ -66,10 +66,11 @@ export interface AppDataShape { export interface WizardDataShape { chosenBundler: WizardBundler | null - chosenFramework: WizardFrontendFramework | null + chosenFramework: Cypress.ResolvedComponentFrameworkDefinition | null chosenManualInstall: boolean detectedBundler: WizardBundler | null - detectedFramework: WizardFrontendFramework | null + detectedFramework: Cypress.ResolvedComponentFrameworkDefinition | null + frameworks: Cypress.ResolvedComponentFrameworkDefinition[] } export interface MigrationDataShape { @@ -197,6 +198,8 @@ export function makeCoreData (modeOptions: Partial = {}): CoreDa chosenManualInstall: false, detectedBundler: null, detectedFramework: null, + // TODO: API to add third party frameworks to this list. + frameworks: CT_FRAMEWORKS.map((framework) => resolveComponentFrameworkDefinition(framework)), }, migration: { step: 'renameAuto', diff --git a/packages/data-context/src/sources/WizardDataSource.ts b/packages/data-context/src/sources/WizardDataSource.ts index ea287193db87..9a400a32177c 100644 --- a/packages/data-context/src/sources/WizardDataSource.ts +++ b/packages/data-context/src/sources/WizardDataSource.ts @@ -1,6 +1,5 @@ import { WIZARD_DEPENDENCY_TYPESCRIPT, - DependencyToInstall, isDependencyInstalled, } from '@packages/scaffold-config' import type { DataContext } from '..' @@ -8,12 +7,12 @@ import type { DataContext } from '..' export class WizardDataSource { constructor (private ctx: DataContext) {} - async packagesToInstall (): Promise { + async packagesToInstall (): Promise { if (!this.ctx.coreData.wizard.chosenFramework || !this.ctx.coreData.wizard.chosenBundler || !this.ctx.currentProject) { return [] } - const packages: DependencyToInstall[] = [ + const packages: Cypress.DependencyToInstall[] = [ ...(await this.ctx.coreData.wizard.chosenFramework.dependencies( this.ctx.coreData.wizard.chosenBundler.type, this.ctx.currentProject, )), diff --git a/packages/data-context/test/unit/codegen/code-generator.spec.ts b/packages/data-context/test/unit/codegen/code-generator.spec.ts index be244325e826..484e0956dff2 100644 --- a/packages/data-context/test/unit/codegen/code-generator.spec.ts +++ b/packages/data-context/test/unit/codegen/code-generator.spec.ts @@ -10,7 +10,7 @@ import { import { SpecOptions } from '../../../src/codegen/spec-options' import templates from '../../../src/codegen/templates' import { createTestDataContext } from '../helper' -import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config' +import { CT_FRAMEWORKS } from '@packages/scaffold-config' import { defaultSpecPattern } from '@packages/config' const tmpPath = path.join(__dirname, 'tmp/test-code-gen') @@ -352,7 +352,7 @@ describe('code-generator', () => { currentProject: 'path/to/myProject', codeGenPath: path.join(__dirname, 'files', 'react', 'Button.jsx'), codeGenType: 'component', - framework: WIZARD_FRAMEWORKS[1], + framework: CT_FRAMEWORKS[1], isDefaultSpecPattern: true, specPattern: [defaultSpecPattern.component], }) diff --git a/packages/data-context/test/unit/codegen/spec-options.spec.ts b/packages/data-context/test/unit/codegen/spec-options.spec.ts index 083f85303771..1027ca06a020 100644 --- a/packages/data-context/test/unit/codegen/spec-options.spec.ts +++ b/packages/data-context/test/unit/codegen/spec-options.spec.ts @@ -1,5 +1,5 @@ import { defaultSpecPattern } from '@packages/config' -import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config' +import { CT_FRAMEWORKS } from '@packages/scaffold-config' import { expect } from 'chai' import fs from 'fs-extra' import path from 'path' @@ -89,7 +89,7 @@ describe('spec-options', () => { codeGenPath: `${tmpPath}/MyComponent.vue`, codeGenType: 'component', isDefaultSpecPattern: true, - framework: WIZARD_FRAMEWORKS[1], + framework: CT_FRAMEWORKS[1], specPattern: [defaultSpecPattern.component], }) @@ -108,7 +108,7 @@ describe('spec-options', () => { codeGenPath: `${tmpPath}/MyComponent.vue`, codeGenType: 'component', isDefaultSpecPattern: true, - framework: WIZARD_FRAMEWORKS[1], + framework: CT_FRAMEWORKS[1], specPattern: [defaultSpecPattern.component], }) @@ -127,7 +127,7 @@ describe('spec-options', () => { codeGenPath: `${tmpPath}/Counter.tsx`, codeGenType: 'component', isDefaultSpecPattern: true, - framework: WIZARD_FRAMEWORKS[0], + framework: CT_FRAMEWORKS[0], specPattern: [defaultSpecPattern.component], componentName: 'Counter', isDefault: true, @@ -146,7 +146,7 @@ describe('spec-options', () => { codeGenPath: `${tmpPath}/Counter.tsx`, codeGenType: 'component', isDefaultSpecPattern: true, - framework: WIZARD_FRAMEWORKS[0], + framework: CT_FRAMEWORKS[0], specPattern: [defaultSpecPattern.component], componentName: 'View', }) @@ -166,7 +166,7 @@ describe('spec-options', () => { codeGenPath: `${tmpPath}/Counter.tsx`, codeGenType: 'component', isDefaultSpecPattern: true, - framework: WIZARD_FRAMEWORKS[0], + framework: CT_FRAMEWORKS[0], specPattern: [defaultSpecPattern.component], componentName: 'View', }) @@ -212,7 +212,7 @@ describe('spec-options', () => { codeGenPath: `${tmpPath}/${componentPath}`, codeGenType: 'component', isDefaultSpecPattern: false, - framework: WIZARD_FRAMEWORKS[1], + framework: CT_FRAMEWORKS[1], specPattern, specs, }) @@ -322,7 +322,7 @@ describe('spec-options', () => { codeGenType: 'component', isDefaultSpecPattern: true, specPattern: [defaultSpecPattern.component], - framework: WIZARD_FRAMEWORKS[1], + framework: CT_FRAMEWORKS[1], }) await fs.outputFile(`${tmpPath}/${fileName}`, '// foo') diff --git a/packages/data-context/test/unit/sources/WizardDataSource.spec.ts b/packages/data-context/test/unit/sources/WizardDataSource.spec.ts index 6bb6367d8877..d29d5259447c 100644 --- a/packages/data-context/test/unit/sources/WizardDataSource.spec.ts +++ b/packages/data-context/test/unit/sources/WizardDataSource.spec.ts @@ -1,9 +1,9 @@ -import { WizardBundler, WizardFrontendFramework, WIZARD_BUNDLERS, WIZARD_FRAMEWORKS } from '@packages/scaffold-config' +import { WizardBundler, WIZARD_BUNDLERS, CT_FRAMEWORKS, resolveComponentFrameworkDefinition } from '@packages/scaffold-config' import { expect } from 'chai' import { createTestDataContext, scaffoldMigrationProject, removeCommonNodeModules } from '../helper' -function findFramework (type: WizardFrontendFramework['type']) { - return WIZARD_FRAMEWORKS.find((x) => x.type === type)! +function findFramework (type: Cypress.ResolvedComponentFrameworkDefinition['type']) { + return resolveComponentFrameworkDefinition(CT_FRAMEWORKS.find((x) => x.type === type)!) } function findBundler (type: WizardBundler['type']) { diff --git a/packages/data-context/tsconfig.json b/packages/data-context/tsconfig.json index 5dfb0e8b808c..aad9aff5db78 100644 --- a/packages/data-context/tsconfig.json +++ b/packages/data-context/tsconfig.json @@ -17,6 +17,6 @@ "noUnusedLocals": false, "noUncheckedIndexedAccess": true, "importsNotUsedAsValues": "error", - "types": [], + "types": ["cypress"], } } \ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 42f46ac6b9ec..e5f9d6f212c2 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -9,7 +9,6 @@ import type { BreakingErrResult } from '@packages/config' import { humanTime, logError, parseResolvedPattern, pluralize } from './errorUtils' import { errPartial, errTemplate, fmt, theme, PartialErr } from './errTemplate' import { stackWithoutMessage } from './stackUtils' -import type { DependencyToInstall } from '@packages/scaffold-config' import type { ClonedError, ConfigValidationFailureInfo, CypressError, ErrTemplateResult, ErrorLike } from './errorTypes' const ansi_up = new AU() @@ -1714,7 +1713,7 @@ export const AllCypressErrors = { ` }, - COMPONENT_TESTING_MISMATCHED_DEPENDENCIES: (dependencies: DependencyToInstall[]) => { + COMPONENT_TESTING_MISMATCHED_DEPENDENCIES: (dependencies: Cypress.DependencyToInstall[]) => { const deps = dependencies.map((dep) => { if (dep.detectedVersion) { return `\`${dep.dependency.installer}\`. Expected ${dep.dependency.minVersion}, found ${dep.detectedVersion}.` diff --git a/packages/frontend-shared/src/components/Select.cy.tsx b/packages/frontend-shared/src/components/Select.cy.tsx index f7ee25a42a3a..28723f5ab3bf 100644 --- a/packages/frontend-shared/src/components/Select.cy.tsx +++ b/packages/frontend-shared/src/components/Select.cy.tsx @@ -194,7 +194,8 @@ describe('', () => { // Choose an option .then(selectFirstOption) + cy.contains('This is the footer').should('be.visible') + // The options list should be closed cy.get(optionsSelector).should('not.exist') .get(inputSelector).should('have.text', 'Selected') diff --git a/packages/frontend-shared/src/components/Select.vue b/packages/frontend-shared/src/components/Select.vue index d4455eef68d7..8be9cba968f8 100644 --- a/packages/frontend-shared/src/components/Select.vue +++ b/packages/frontend-shared/src/components/Select.vue @@ -136,6 +136,7 @@ + diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json index 8502fb44c0c1..fd85798eb6bc 100644 --- a/packages/frontend-shared/src/locales/en-US.json +++ b/packages/frontend-shared/src/locales/en-US.json @@ -352,11 +352,11 @@ "frameworkLabel": "Front-end framework", "frameworkPlaceholder": "Pick a framework", "bundlerLabel": "Bundler", - "bundlerLabelDescription": "(dev server)", "bundlerPlaceholder": "Pick a bundler", "languageLabel": "Language", "configFileLanguageLabel": "Cypress config file", - "detected": "(detected)" + "detected": "(detected)", + "browseIntegrations": "Browse our list of third-party framework integrations" }, "step": { "continue": "Continue", @@ -937,7 +937,9 @@ }, "versions": { "alpha": "Alpha", + "community": "Community", "beta": "Beta", - "new": "New" + "new": "New", + "communityNote": "This is a community provided definition." } } diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index ae482bce561b..19f2426f3271 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -1324,19 +1324,6 @@ type FileParts implements Node { relative: String! } -enum FrontendFrameworkEnum { - angular - nextjs - nuxtjs - react - reactscripts - svelte - vue2 - vue3 - vueclivue2 - vueclivue3 -} - """Error from generated spec""" type GenerateSpecResponse { """The currently opened project""" @@ -2287,6 +2274,7 @@ type Subscription { enum SupportStatusEnum { alpha beta + community full } @@ -2396,6 +2384,9 @@ type WizardFrontendFramework implements Node { """The category (framework, like react-scripts, or library, like react""" category: String! + """Raw SVG icon of framework""" + icon: String + """Relay style Node ID field for the WizardFrontendFramework field""" id: ID! @@ -2415,7 +2406,7 @@ type WizardFrontendFramework implements Node { supportedBundlers: [WizardBundler!]! """The unique identifier for a framework or library""" - type: FrontendFrameworkEnum! + type: String! } """Details about an NPM Package listed during the wizard install""" @@ -2446,5 +2437,5 @@ type WizardNpmPackage implements Node { input WizardUpdateInput { bundler: SupportedBundlers - framework: FrontendFrameworkEnum + framework: String } diff --git a/packages/graphql/src/schemaTypes/enumTypes/gql-WizardEnums.ts b/packages/graphql/src/schemaTypes/enumTypes/gql-WizardEnums.ts index 02f03f3a0c91..c8fd1d02c706 100644 --- a/packages/graphql/src/schemaTypes/enumTypes/gql-WizardEnums.ts +++ b/packages/graphql/src/schemaTypes/enumTypes/gql-WizardEnums.ts @@ -1,5 +1,5 @@ import { CODE_LANGUAGES } from '@packages/types' -import { WIZARD_FRAMEWORKS, WIZARD_BUNDLERS } from '@packages/scaffold-config' +import { WIZARD_BUNDLERS, SUPPORT_STATUSES } from '@packages/scaffold-config' import { enumType } from 'nexus' export const SupportedBundlerEnum = enumType({ @@ -13,14 +13,9 @@ export const WizardConfigFileStatusEnum = enumType({ members: ['changes', 'valid', 'skipped', 'error'], }) -export const FrontendFrameworkEnum = enumType({ - name: 'FrontendFrameworkEnum', - members: WIZARD_FRAMEWORKS.map((t) => t.type), -}) - export const SupportStatusEnum = enumType({ name: 'SupportStatusEnum', - members: ['alpha', 'beta', 'full'], + members: SUPPORT_STATUSES, }) export const CodeLanguageEnum = enumType({ diff --git a/packages/graphql/src/schemaTypes/inputTypes/gql-WizardUpdateInput.ts b/packages/graphql/src/schemaTypes/inputTypes/gql-WizardUpdateInput.ts index 988ca5d43009..dcc0a2587fd9 100644 --- a/packages/graphql/src/schemaTypes/inputTypes/gql-WizardUpdateInput.ts +++ b/packages/graphql/src/schemaTypes/inputTypes/gql-WizardUpdateInput.ts @@ -1,12 +1,10 @@ import { inputObjectType } from 'nexus' -import { FrontendFrameworkEnum, SupportedBundlerEnum } from '../enumTypes/gql-WizardEnums' +import { SupportedBundlerEnum } from '../enumTypes/gql-WizardEnums' export const WizardUpdateInput = inputObjectType({ name: 'WizardUpdateInput', definition (t) { - t.field('framework', { - type: FrontendFrameworkEnum, - }) + t.string('framework') t.field('bundler', { type: SupportedBundlerEnum, diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index daf950a4ea0f..1a1ed01de9b5 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -10,7 +10,6 @@ import { GenerateSpecResponse } from './gql-GenerateSpecResponse' import { Cohort, CohortInput } from './gql-Cohorts' import { Query } from './gql-Query' import { ScaffoldedFile } from './gql-ScaffoldedFile' -import { WIZARD_BUNDLERS, WIZARD_FRAMEWORKS } from '@packages/scaffold-config' import debugLib from 'debug' import { ReactComponentResponse } from './gql-ReactComponentResponse' import { TestsBySpecInput } from '../inputTypes' @@ -183,6 +182,7 @@ export const mutation = mutationType({ if (ctx.coreData.currentTestingType && !ctx.lifecycleManager.isTestingTypeConfigured(ctx.coreData.currentTestingType)) { // Component Testing has a wizard to help users configure their project if (ctx.coreData.currentTestingType === 'component') { + await ctx.actions.wizard.detectFrameworks() await ctx.actions.wizard.initialize() } else { // E2E doesn't have such a wizard, we just create/update their cypress.config.js. @@ -218,11 +218,11 @@ export const mutation = mutationType({ }, resolve: async (source, args, ctx) => { if (args.input.framework) { - ctx.actions.wizard.setFramework(WIZARD_FRAMEWORKS.find((x) => x.type === args.input.framework) ?? null) + ctx.actions.wizard.setFramework(ctx.coreData.wizard.frameworks.find((x) => x.type === args.input.framework) ?? null) } if (args.input.bundler) { - ctx.actions.wizard.setBundler(WIZARD_BUNDLERS.find((x) => x.type === args.input.bundler) ?? null) + ctx.actions.wizard.setBundler(args.input.bundler) } // TODO: remove when live-mutations are implements diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Wizard.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Wizard.ts index 65092d21a6b6..dae2afb2bcb2 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Wizard.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Wizard.ts @@ -2,7 +2,7 @@ import { WizardBundler } from './gql-WizardBundler' import { WizardFrontendFramework } from './gql-WizardFrontendFramework' import { WizardNpmPackage } from './gql-WizardNpmPackage' import { objectType } from 'nexus' -import { WIZARD_BUNDLERS, WIZARD_FRAMEWORKS } from '@packages/scaffold-config' +import { WIZARD_BUNDLERS } from '@packages/scaffold-config' export const Wizard = objectType({ name: 'Wizard', @@ -27,7 +27,7 @@ export const Wizard = objectType({ t.nonNull.list.nonNull.field('frameworks', { type: WizardFrontendFramework, description: 'All of the component testing frameworks to choose from', - resolve: () => Array.from(WIZARD_FRAMEWORKS), // TODO(tim): fix this in nexus to accept Readonly + resolve: (source, args, ctx) => Array.from(ctx.coreData.wizard.frameworks), }) t.nonNull.list.nonNull.field('packagesToInstall', { diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-WizardFrontendFramework.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-WizardFrontendFramework.ts index 50a34855fd4d..d61c186f5ddb 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-WizardFrontendFramework.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-WizardFrontendFramework.ts @@ -1,22 +1,30 @@ import { WizardBundler } from './gql-WizardBundler' -import { FrontendFrameworkEnum, SupportStatusEnum } from '../enumTypes/gql-WizardEnums' import { objectType } from 'nexus' +import { WIZARD_BUNDLERS } from '@packages/scaffold-config' +import { SupportStatusEnum } from '../enumTypes' export const WizardFrontendFramework = objectType({ name: 'WizardFrontendFramework', description: 'A frontend framework that we can setup within the app', node: 'type', definition (t) { - t.nonNull.field('type', { - type: FrontendFrameworkEnum, + t.nonNull.string('type', { description: 'The unique identifier for a framework or library', }), - t.nonNull.field('category', { - type: 'String', + t.nonNull.string('category', { description: 'The category (framework, like react-scripts, or library, like react', }), + t.nonNull.string('name', { + description: 'The display name of the framework', + }) + + t.nonNull.field('supportStatus', { + description: 'Current support status of the framework', + type: SupportStatusEnum, + }) + t.nonNull.boolean('isSelected', { description: 'Whether this is the selected framework in the wizard', resolve: (source, args, ctx) => ctx.wizardData.chosenFramework?.type === source.type, @@ -27,26 +35,26 @@ export const WizardFrontendFramework = objectType({ resolve: (source, args, ctx) => ctx.wizardData.detectedFramework?.type === source.type, }) - t.nonNull.string('name', { - description: 'The display name of the framework', - }) - - t.nonNull.field('supportStatus', { - description: 'Current support status of the framework', - type: SupportStatusEnum, - }) - t.nonNull.list.nonNull.field('supportedBundlers', { type: WizardBundler, description: 'All of the supported bundlers for this framework', resolve: (source, args, ctx) => { - return [...source.supportedBundlers] + const findBundler = (type: 'webpack' | 'vite') => { + const b = WIZARD_BUNDLERS.find((b) => b.type === type) + + if (!b) { + throw Error(`Invalid bundler: ${type}`) + } + + return b + } + + return ctx.wizardData.chosenFramework?.supportedBundlers.map(findBundler) ?? [] }, }) - }, - sourceType: { - module: '@packages/scaffold-config', - export: 'WizardFrontendFramework', + t.string('icon', { + description: 'Raw SVG icon of framework', + }) }, }) diff --git a/packages/launchpad/cypress/e2e/project-setup.cy.ts b/packages/launchpad/cypress/e2e/project-setup.cy.ts index dab37fce599e..2ba23a94e0f2 100644 --- a/packages/launchpad/cypress/e2e/project-setup.cy.ts +++ b/packages/launchpad/cypress/e2e/project-setup.cy.ts @@ -508,7 +508,7 @@ describe('Launchpad: Setup Project', () => { cy.contains('Pick a framework').click() cy.findByRole('option', { name: 'Vue.js 3' }).click() - cy.findByRole('button', { name: 'Bundler(dev server) Pick a bundler' }).click() + cy.findByRole('button', { name: 'Bundler Pick a bundler' }).click() cy.findByRole('option', { name: 'Vite' }).click() cy.findByRole('button', { name: 'Next step' }).should('not.have.disabled') diff --git a/packages/launchpad/cypress/e2e/scaffold-component-testing.cy.ts b/packages/launchpad/cypress/e2e/scaffold-component-testing.cy.ts index 862998098536..bf58ec327960 100644 --- a/packages/launchpad/cypress/e2e/scaffold-component-testing.cy.ts +++ b/packages/launchpad/cypress/e2e/scaffold-component-testing.cy.ts @@ -149,4 +149,68 @@ describe('scaffolding component testing', { verifyConfigFile(`cypress.config.js`) }) }) + + context('3rd party ct plugin', () => { + it('Scaffolds component testing for Qwik using Vite', () => { + cy.scaffoldProject('qwik-app') + cy.openProject('qwik-app') + + cy.withCtx(async (ctx) => { + await ctx.actions.file.removeFileInProject('./node_modules/cypress-ct-qwik') + await ctx.actions.file.moveFileInProject('./cypress-ct-qwik', './node_modules/cypress-ct-qwik') + }) + + cy.visitLaunchpad() + cy.skipWelcome() + + cy.contains('Component Testing').click() + cy.contains('button', 'Qwik').should('be.visible') + cy.contains('button', 'Next step').click() + + cy.findByTestId('dependencies-to-install').within(() => { + cy.contains('li', '@builder.io/qwik').within(() => { + cy.findByLabelText('installed') + }) + + cy.contains('li', 'vite').within(() => { + cy.findByLabelText('installed') + }) + }) + + cy.contains('button', 'Continue').click() + + verifyConfigFile('cypress.config.js') + }) + + it('Scaffolds component testing for Solid using Vite', () => { + cy.scaffoldProject('ct-public-api-solid-js') + cy.openProject('ct-public-api-solid-js') + + cy.withCtx(async (ctx) => { + await ctx.actions.file.removeFileInProject('./node_modules/cypress-ct-solid-js') + await ctx.actions.file.moveFileInProject('./cypress-ct-solid-js', './node_modules/cypress-ct-solid-js') + }) + + cy.visitLaunchpad() + cy.skipWelcome() + + cy.contains('Component Testing').click() + cy.contains('button', 'Solid').should('be.visible') + cy.contains('button', 'Next step').click() + + cy.findByTestId('dependencies-to-install').within(() => { + cy.contains('li', 'solid-js').within(() => { + cy.findByLabelText('installed') + }) + + cy.contains('li', 'vite').within(() => { + cy.findByLabelText('installed') + }) + }) + + cy.contains('button', 'Continue').click() + + verifyConfigFile('cypress.config.js') + }) + }) }) diff --git a/packages/launchpad/cypress/e2e/scaffold-project.cy.ts b/packages/launchpad/cypress/e2e/scaffold-project.cy.ts index 92085d5e9e13..855f25ed4b81 100644 --- a/packages/launchpad/cypress/e2e/scaffold-project.cy.ts +++ b/packages/launchpad/cypress/e2e/scaffold-project.cy.ts @@ -1,4 +1,3 @@ -import type { WizardFrontendFramework } from '@packages/scaffold-config' import type { SnapshotScaffoldTestResult } from '@packages/launchpad/cypress/tasks/snapshotsScaffold' // The tests in this file take an existing project without Cypress Configured @@ -56,8 +55,8 @@ function scaffoldAndOpenE2EProject (opts: { function scaffoldAndOpenCTProject (opts: { name: Parameters[0] - framework: WizardFrontendFramework['name'] - bundler?: WizardFrontendFramework['supportedBundlers'][number]['name'] + framework: Cypress.ResolvedComponentFrameworkDefinition['name'] + bundler?: Cypress.ResolvedComponentFrameworkDefinition['supportedBundlers'][number] args?: Parameters[1] removeFixturesFolder?: boolean }) { diff --git a/packages/launchpad/package.json b/packages/launchpad/package.json index 72aba3e73b74..76e27db14a42 100644 --- a/packages/launchpad/package.json +++ b/packages/launchpad/package.json @@ -28,6 +28,7 @@ "@iconify/vue": "3.0.0-beta.1", "@intlify/vite-plugin-vue-i18n": "2.4.0", "@packages/frontend-shared": "0.0.0-development", + "@packages/scaffold-config": "0.0.0-development", "@percy/cypress": "^3.1.0", "@purge-icons/generated": "0.8.1", "@testing-library/cypress": "9.0.0", diff --git a/packages/launchpad/src/setup/CommunityLabel.vue b/packages/launchpad/src/setup/CommunityLabel.vue new file mode 100644 index 000000000000..bac87d4e75b6 --- /dev/null +++ b/packages/launchpad/src/setup/CommunityLabel.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/launchpad/src/setup/EnvironmentSetup.cy.tsx b/packages/launchpad/src/setup/EnvironmentSetup.cy.tsx index 30e68dff59b6..1644b018e2df 100644 --- a/packages/launchpad/src/setup/EnvironmentSetup.cy.tsx +++ b/packages/launchpad/src/setup/EnvironmentSetup.cy.tsx @@ -54,6 +54,11 @@ describe('', { viewportWidth: 800 }, () => { cy.findByRole('button', { name: 'Next step' }) .should('have.disabled') + + cy.findByRole('link', { name: 'Browse our list of third-party framework integrations' }) + .should('have.attr', 'href', 'https://on.cypress.io/component-integrations?utm_medium=Select+Framework+Dropdown&utm_source=Binary%3A+Launchpad&utm_campaign=Browse+third-party+frameworks') + + cy.percySnapshot() }) it('renders the detected flag', () => { @@ -97,6 +102,6 @@ describe('', { viewportWidth: 800 }, () => { ), }) - cy.findByLabelText('Bundler(dev server)').should('be.visible') + cy.findByLabelText('Bundler').should('be.visible') }) }) diff --git a/packages/launchpad/src/setup/EnvironmentSetup.vue b/packages/launchpad/src/setup/EnvironmentSetup.vue index b948be9ba137..8241ad9344f6 100644 --- a/packages/launchpad/src/setup/EnvironmentSetup.vue +++ b/packages/launchpad/src/setup/EnvironmentSetup.vue @@ -22,7 +22,6 @@ :value="props.gql.bundler?.type ?? undefined" :placeholder="t('setupPage.projectSetup.bundlerPlaceholder')" :label="t('setupPage.projectSetup.bundlerLabel')" - :description="t('setupPage.projectSetup.bundlerLabelDescription')" selector-type="bundler" data-testid="select-bundler" @select-bundler="val => onWizardSetup('bundler', val)" @@ -44,6 +43,7 @@ import { import { useI18n } from '@cy/i18n' import { useMutation } from '@urql/vue' +import type { FrameworkOption } from './types' gql` fragment EnvironmentSetup on Wizard { @@ -65,6 +65,7 @@ fragment EnvironmentSetup on Wizard { isDetected } category + icon } frameworks { id @@ -73,6 +74,7 @@ fragment EnvironmentSetup on Wizard { isDetected type category + icon } allBundlers { id @@ -97,7 +99,20 @@ const bundlers = computed(() => { }) const frameworks = computed(() => { - return (props.gql.frameworks || []).map((x) => ({ ...x })).sort((x, y) => x.name.localeCompare(y.name)) + const data = (props.gql.frameworks || []).map((x) => { + return { + type: x.type, + supportStatus: x.supportStatus, + name: x.name, + id: x.id, + isDetected: x.isDetected, + icon: x.icon || undefined, + } + }) + + data.sort((x, y) => x.name.localeCompare(y.name)) + + return data }) gql` diff --git a/packages/launchpad/src/setup/FrameworkOptionsFooter.vue b/packages/launchpad/src/setup/FrameworkOptionsFooter.vue new file mode 100644 index 000000000000..38aea0650f40 --- /dev/null +++ b/packages/launchpad/src/setup/FrameworkOptionsFooter.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/launchpad/src/setup/ManualInstall.vue b/packages/launchpad/src/setup/ManualInstall.vue index 72c113bd3093..2f0d824c0d25 100644 --- a/packages/launchpad/src/setup/ManualInstall.vue +++ b/packages/launchpad/src/setup/ManualInstall.vue @@ -17,7 +17,7 @@ class="px-24px" :class="{ 'border-t border-t-gray-100': !!props.gql.wizard.installDependenciesCommand }" > -
    +
    • = [ { @@ -11,7 +12,6 @@ const manyOptions: Readonly = [ }, { name: 'React.js', - description: '(detected)', id: 'react', type: 'react', supportStatus: 'alpha', @@ -72,6 +72,26 @@ describe('', () => { cy.contains('button', 'placeholder').should('exist') }) + it('shows a community integration', () => { + cy.mount(() => ( + + )) + + cy.percySnapshot() + }) + it('should select the value', () => { cy.mount(() => ( diff --git a/packages/launchpad/src/setup/SelectFwOrBundler.vue b/packages/launchpad/src/setup/SelectFwOrBundler.vue index 8fd5947bb00b..45c9e530d809 100644 --- a/packages/launchpad/src/setup/SelectFwOrBundler.vue +++ b/packages/launchpad/src/setup/SelectFwOrBundler.vue @@ -25,8 +25,14 @@ > +
      + - -