mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-24 07:59:12 -05:00
feat: index.html configurability and storybook support (#18242)
This commit is contained in:
@@ -5,15 +5,17 @@
|
||||
"start": "react-scripts start",
|
||||
"test": "node ../../../../scripts/cypress run-ct"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/react": "file:../../dist",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"mocha-junit-reporter": "^2.0.0",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"typescript": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
+6
@@ -8,6 +8,12 @@ declare namespace legacyDevServer {
|
||||
* @returns modified final configuration
|
||||
*/
|
||||
setWebpackConfig?(config:Configuration): Configuration
|
||||
|
||||
/**
|
||||
* Path to an index.html file that will serve as the template in
|
||||
* which your components will be rendered.
|
||||
*/
|
||||
indexHtml?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,11 @@ const { startDevServer } = require('@cypress/webpack-dev-server')
|
||||
const getBabelWebpackConfig = require('./getBabelWebpackConfig')
|
||||
const { getLegacyDevServer } = require('../utils/legacy-setup-dev-server')
|
||||
|
||||
function devServer (cypressDevServerConfig, devServerConfig) {
|
||||
function devServer (cypressDevServerConfig, devServerConfig = {}) {
|
||||
return startDevServer({
|
||||
options: cypressDevServerConfig,
|
||||
webpackConfig: getBabelWebpackConfig(cypressDevServerConfig.config, devServerConfig),
|
||||
indexHtml: devServerConfig.indexHtml,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Vendored
+6
@@ -4,6 +4,12 @@ declare namespace legacyDevServer {
|
||||
* The object exported of your craco.config.js file
|
||||
*/
|
||||
cracoConfig: any
|
||||
|
||||
/**
|
||||
* Path to an index.html file that will serve as the template in
|
||||
* which your components will be rendered.
|
||||
*/
|
||||
indexHtml?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,11 @@ const { startDevServer } = require('@cypress/webpack-dev-server')
|
||||
const { createWebpackDevConfig } = require('@craco/craco')
|
||||
const { getLegacyDevServer } = require('../utils/legacy-setup-dev-server')
|
||||
|
||||
function devServer (cypressDevServerConfig, cracoConfig) {
|
||||
function devServer (cypressDevServerConfig, cracoConfig, indexHtml) {
|
||||
return startDevServer({
|
||||
options: cypressDevServerConfig,
|
||||
webpackConfig: createWebpackDevConfig(cracoConfig),
|
||||
indexHtml,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,8 +19,8 @@ module.exports = getLegacyDevServer(devServer, (config) => {
|
||||
|
||||
// New signature
|
||||
// - Note that this also includes a change to the second argument!
|
||||
module.exports.devServer = (cypressDevServerConfig, { cracoConfig }) => {
|
||||
return devServer(cypressDevServerConfig, cracoConfig)
|
||||
module.exports.devServer = (cypressDevServerConfig, { cracoConfig, indexHtml }) => {
|
||||
return devServer(cypressDevServerConfig, cracoConfig, indexHtml)
|
||||
}
|
||||
|
||||
module.exports.defineDevServerConfig = function (devServerConfig) {
|
||||
|
||||
+6
@@ -4,6 +4,12 @@ declare namespace legacyDevServer {
|
||||
* Location of the weppack.config Cypress should use
|
||||
*/
|
||||
webpackFilename?: string
|
||||
|
||||
/**
|
||||
* Path to an index.html file that will serve as the template in
|
||||
* which your components will be rendered.
|
||||
*/
|
||||
indexHtml?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ function normalizeWebpackPath (config, webpackConfigPath) {
|
||||
* **Important:** `webpackFilename` path is relative to the project root (cypress.json location)
|
||||
* @type {(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions, options: { webpackFilename: string }) => Cypress.PluginConfigOptions}
|
||||
*/
|
||||
function devServer (cypressDevServerConfig, { webpackFilename }) {
|
||||
function devServer (cypressDevServerConfig, { webpackFilename, indexHtml }) {
|
||||
const webpackConfig = tryLoadWebpackConfig(normalizeWebpackPath(cypressDevServerConfig.config, webpackFilename))
|
||||
|
||||
if (!webpackConfig) {
|
||||
@@ -25,6 +25,7 @@ function devServer (cypressDevServerConfig, { webpackFilename }) {
|
||||
return startDevServer({
|
||||
options: cypressDevServerConfig,
|
||||
webpackConfig,
|
||||
indexHtml,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Vendored
+22
-1
@@ -6,12 +6,33 @@
|
||||
declare function legacyDevServer(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): void
|
||||
|
||||
declare namespace legacyDevServer {
|
||||
interface CypressNextDevServerConfig {
|
||||
/**
|
||||
* Path to an index.html file that will serve as the template in
|
||||
* which your components will be rendered.
|
||||
*/
|
||||
indexHtml?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Type helper to make writing `CypressNextDevServerConfig` easier
|
||||
*/
|
||||
function defineDevServerConfig(devServerConfig: CypressNextDevServerConfig): CypressNextDevServerConfig
|
||||
|
||||
/**
|
||||
* Sets up a Cypress component testing environment for your NextJs application
|
||||
* @param cypressDevServerConfig comes from the `devServer()` function first argument
|
||||
* @returns the resolved dev server object that cypress can use to start testing
|
||||
*/
|
||||
function devServer(cypressDevServerConfig: Cypress.DevServerConfig): Cypress.ResolvedDevServerConfig
|
||||
function devServer(cypressDevServerConfig: Cypress.DevServerConfig, devServerConfig?: CypressNextDevServerConfig): Cypress.ResolvedDevServerConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a Cypress component testing environment for your NextJs application
|
||||
* @param on comes from the argument of the `pluginsFile` function
|
||||
* @param config comes from the argument of the `pluginsFile` function
|
||||
* @param devServerConfig additional config object (create an empty object to see how to use it)
|
||||
*/
|
||||
declare function legacyDevServer(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions, devServerConfig?: legacyDevServer.CypressNextDevServerConfig): void
|
||||
|
||||
export = legacyDevServer;
|
||||
@@ -2,7 +2,7 @@ const path = require('path')
|
||||
const findNextWebpackConfig = require('./findNextWebpackConfig')
|
||||
const { getLegacyDevServer } = require('../utils/legacy-setup-dev-server')
|
||||
|
||||
async function devServer (cypressDevServerConfig) {
|
||||
async function devServer (cypressDevServerConfig, { indexHtml } = {}) {
|
||||
const webpackConfig = await findNextWebpackConfig(cypressDevServerConfig.config)
|
||||
|
||||
// require('webpack') now points to nextjs bundled version
|
||||
@@ -11,7 +11,7 @@ async function devServer (cypressDevServerConfig) {
|
||||
return startDevServer({
|
||||
options: cypressDevServerConfig,
|
||||
webpackConfig,
|
||||
template: path.resolve(__dirname, 'index-template.html'),
|
||||
indexHtml: indexHtml || path.resolve(__dirname, 'index-template.html'),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -4,6 +4,12 @@ declare namespace legacyDevServer {
|
||||
* Location of the weppack.config Cypress should use
|
||||
*/
|
||||
webpackConfigPath?: string
|
||||
|
||||
/**
|
||||
* Path to an index.html file that will serve as the template in
|
||||
* which your components will be rendered.
|
||||
*/
|
||||
indexHtml?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-4
@@ -4,14 +4,14 @@ const { getLegacyDevServer } = require('../utils/legacy-setup-dev-server')
|
||||
|
||||
function devServer (cypressDevServerConfig, {
|
||||
webpackConfigPath,
|
||||
} = {
|
||||
webpackConfigPath: 'react-scripts/config/webpack.config',
|
||||
}) {
|
||||
indexHtml,
|
||||
} = {}) {
|
||||
return startDevServer({
|
||||
options: cypressDevServerConfig,
|
||||
webpackConfig: findReactScriptsWebpackConfig(cypressDevServerConfig.config, {
|
||||
webpackConfigPath,
|
||||
webpackConfigPath: webpackConfigPath || 'react-scripts/config/webpack.config',
|
||||
}),
|
||||
indexHtml,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export const makeCypressPlugin = (
|
||||
supportFilePath: string | false,
|
||||
devServerEvents: NodeJS.EventEmitter,
|
||||
specs: Spec[],
|
||||
indexHtml?: string,
|
||||
): Plugin => {
|
||||
let base = '/'
|
||||
|
||||
@@ -47,6 +48,7 @@ export const makeCypressPlugin = (
|
||||
})
|
||||
|
||||
const posixSupportFilePath = supportFilePath ? convertPathToPosix(resolve(projectRoot, supportFilePath)) : undefined
|
||||
const posixIndexHtml = indexHtml ? convertPathToPosix(resolve(projectRoot, indexHtml)) : undefined
|
||||
|
||||
const normalizedSupportFilePath = posixSupportFilePath ? `${base}@fs/${posixSupportFilePath}` : undefined
|
||||
|
||||
@@ -66,29 +68,42 @@ export const makeCypressPlugin = (
|
||||
configResolved (config) {
|
||||
base = config.base
|
||||
},
|
||||
transformIndexHtml () {
|
||||
async transformIndexHtml () {
|
||||
debug('transformIndexHtml with base', base)
|
||||
const indexHtmlPath = indexHtml ? resolve(projectRoot, indexHtml) : resolve(__dirname, '..', 'index.html')
|
||||
const indexHtmlContent = await read(indexHtmlPath, { encoding: 'utf8' })
|
||||
|
||||
return [
|
||||
return {
|
||||
html: indexHtmlContent,
|
||||
// load the script at the end of the body
|
||||
// script has to be loaded when the vite client is connected
|
||||
{
|
||||
tags: [{
|
||||
tag: 'script',
|
||||
injectTo: 'body',
|
||||
attrs: { type: 'module' },
|
||||
children: `import(${JSON.stringify(`${base}@fs/${INIT_FILEPATH}`)})`,
|
||||
},
|
||||
]
|
||||
}],
|
||||
}
|
||||
},
|
||||
configureServer: async (server: ViteDevServer) => {
|
||||
const indexHtml = await read(resolve(__dirname, '..', 'index.html'), { encoding: 'utf8' })
|
||||
server.middlewares.use(`${base}index.html`, async (req, res) => {
|
||||
const transformedIndexHtml = await server.transformIndexHtml(base, '')
|
||||
|
||||
const transformedIndexHtml = await server.transformIndexHtml(base, indexHtml)
|
||||
|
||||
server.middlewares.use(`${base}index.html`, (req, res) => res.end(transformedIndexHtml))
|
||||
return res.end(transformedIndexHtml)
|
||||
})
|
||||
},
|
||||
handleHotUpdate: ({ server, file }) => {
|
||||
debug('handleHotUpdate - file', file)
|
||||
|
||||
// If the user provided IndexHtml is changed, do a full-reload
|
||||
if (file === posixIndexHtml) {
|
||||
server.ws.send({
|
||||
type: 'full-reload',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// get the graph node for the file that just got updated
|
||||
let moduleImporters = server.moduleGraph.fileToModulesMap.get(file)
|
||||
let iterationNumber = 0
|
||||
|
||||
@@ -18,9 +18,15 @@ export interface StartDevServerOptions {
|
||||
* @optional
|
||||
*/
|
||||
viteConfig?: Omit<InlineConfig, 'base' | 'root'>
|
||||
|
||||
/**
|
||||
* Path to an index.html file that will serve as the template in
|
||||
* which your components will be rendered.
|
||||
*/
|
||||
indexHtml?: string
|
||||
}
|
||||
|
||||
const resolveServerConfig = async ({ viteConfig, options }: StartDevServerOptions): Promise<InlineConfig> => {
|
||||
const resolveServerConfig = async ({ viteConfig, options, indexHtml }: StartDevServerOptions): Promise<InlineConfig> => {
|
||||
const { projectRoot, supportFile } = options.config
|
||||
|
||||
const requiredOptions: InlineConfig = {
|
||||
@@ -30,7 +36,7 @@ const resolveServerConfig = async ({ viteConfig, options }: StartDevServerOption
|
||||
|
||||
const finalConfig: InlineConfig = { ...viteConfig, ...requiredOptions }
|
||||
|
||||
finalConfig.plugins = [...(finalConfig.plugins || []), makeCypressPlugin(projectRoot, supportFile, options.devServerEvents, options.specs)]
|
||||
finalConfig.plugins = [...(finalConfig.plugins || []), makeCypressPlugin(projectRoot, supportFile, options.devServerEvents, options.specs, indexHtml)]
|
||||
|
||||
// This alias is necessary to avoid a "prefixIdentifiers" issue from slots mounting
|
||||
// only cjs compiler-core accepts using prefixIdentifiers in slots which vue test utils use.
|
||||
|
||||
@@ -20,14 +20,14 @@ export interface UserWebpackDevServerOptions {
|
||||
interface MakeWebpackConfigOptions extends CypressCTOptionsPluginOptionsWithEmitter, UserWebpackDevServerOptions {
|
||||
devServerPublicPathRoute: string
|
||||
isOpenMode: boolean
|
||||
template?: string
|
||||
indexHtml?: string
|
||||
}
|
||||
|
||||
const OsSeparatorRE = RegExp(`\\${path.sep}`, 'g')
|
||||
const posixSeparator = '/'
|
||||
|
||||
export async function makeWebpackConfig (userWebpackConfig: webpack.Configuration, options: MakeWebpackConfigOptions): Promise<webpack.Configuration> {
|
||||
const { projectRoot, devServerPublicPathRoute, files, supportFile, devServerEvents, template } = options
|
||||
const { projectRoot, devServerPublicPathRoute, files, supportFile, devServerEvents, indexHtml } = options
|
||||
|
||||
debug(`User passed in webpack config with values %o`, userWebpackConfig)
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function makeWebpackConfig (userWebpackConfig: webpack.Configuratio
|
||||
|
||||
const mergedConfig = merge<webpack.Configuration>(
|
||||
userWebpackConfig,
|
||||
makeDefaultWebpackConfig(template),
|
||||
makeDefaultWebpackConfig(indexHtml),
|
||||
dynamicWebpackConfig,
|
||||
)
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ export interface StartDevServer extends UserWebpackDevServerOptions {
|
||||
/* support passing a path to the user's webpack config */
|
||||
webpackConfig?: Record<string, any>
|
||||
/* base html template to render in AUT */
|
||||
template?: string
|
||||
indexHtml?: string
|
||||
}
|
||||
|
||||
const debug = Debug('cypress:webpack-dev-server:start')
|
||||
|
||||
export async function start ({ webpackConfig: userWebpackConfig, template, options, ...userOptions }: StartDevServer, exitProcess = process.exit): Promise<WebpackDevServer> {
|
||||
export async function start ({ webpackConfig: userWebpackConfig, indexHtml, options, ...userOptions }: StartDevServer, exitProcess = process.exit): Promise<WebpackDevServer> {
|
||||
if (!userWebpackConfig) {
|
||||
debug('User did not pass in any webpack configuration')
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function start ({ webpackConfig: userWebpackConfig, template, optio
|
||||
|
||||
const webpackConfig = await makeWebpackConfig(userWebpackConfig || {}, {
|
||||
files: options.specs,
|
||||
template,
|
||||
indexHtml,
|
||||
projectRoot,
|
||||
devServerPublicPathRoute,
|
||||
devServerEvents: options.devServerEvents,
|
||||
|
||||
@@ -5,7 +5,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
* @param {string} [template] - base template to use
|
||||
* @returns {import('webpack').Configuration}
|
||||
*/
|
||||
module.exports = function makeDefaultConfig (template) {
|
||||
module.exports = function makeDefaultConfig (indexHtml) {
|
||||
return {
|
||||
mode: 'development',
|
||||
optimization: {
|
||||
@@ -18,7 +18,7 @@ module.exports = function makeDefaultConfig (template) {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
plugins: [new HtmlWebpackPlugin({
|
||||
template: template || path.resolve(__dirname, '..', 'index-template.html'),
|
||||
template: indexHtml || path.resolve(__dirname, '..', 'index-template.html'),
|
||||
})],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@@ -44,6 +44,7 @@ const navigation = [
|
||||
{ name: 'Specs', icon: SpecsIcon, href: '/' },
|
||||
{ name: 'Runs', icon: CodeIcon, href: '/runner' },
|
||||
{ name: 'Settings', icon: SettingsIcon, href: '/settings' },
|
||||
{ name: 'New Spec', icon: SettingsIcon, href: '/newspec' },
|
||||
]
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div v-if="query.data.value?.wizard.storybook?.configured">
|
||||
<h2>New Spec</h2>
|
||||
<ul v-if="query.data.value.wizard.storybook.stories.length">
|
||||
<li
|
||||
v-for="story of query.data.value.wizard.storybook.stories"
|
||||
:key="story"
|
||||
@click="storyClick(story)"
|
||||
>
|
||||
{{ story }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>
|
||||
No Stories Detected
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
Storybook is not configured for this project
|
||||
</div>
|
||||
</template>
|
||||
<route>
|
||||
{
|
||||
name: "New Spec Page"
|
||||
}
|
||||
</route>
|
||||
<script lang="ts" setup>
|
||||
import { gql, useMutation, useQuery } from '@urql/vue'
|
||||
import { MainQueryDocument, GenerateSpecFromStoryDocument } from '../generated/graphql'
|
||||
|
||||
gql`
|
||||
query MainQuery {
|
||||
wizard {
|
||||
storybook {
|
||||
configured
|
||||
stories
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation GenerateSpecFromStory($storyPath: String!) {
|
||||
generateSpecFromStory (storyPath: $storyPath) {
|
||||
storybook {
|
||||
configured,
|
||||
generatedSpec
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const query = useQuery({ query: MainQueryDocument })
|
||||
const mutation = useMutation(GenerateSpecFromStoryDocument)
|
||||
|
||||
async function storyClick (story) {
|
||||
await mutation.executeMutation({ storyPath: story })
|
||||
const generatedSpec = mutation.data.value?.generateSpecFromStory.storybook?.generatedSpec
|
||||
|
||||
// Runner doesn't pick up new file without timeout, I'm guessing a race condition between file watcher and runner starting
|
||||
setTimeout(() => {
|
||||
window.location.href = `${window.location.origin}/__/#/tests/component/${generatedSpec}`
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
@@ -18,6 +18,7 @@
|
||||
"devDependencies": {
|
||||
"@packages/ts": "0.0.0-development",
|
||||
"@packages/types": "0.0.0-development",
|
||||
"@storybook/csf-tools": "^6.4.0-alpha.38",
|
||||
"mocha": "7.0.1",
|
||||
"rimraf": "3.0.2"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DataContext } from '.'
|
||||
import { AppActions, ProjectActions, WizardActions } from './actions'
|
||||
import { AppActions, ProjectActions, StorybookActions, WizardActions } from './actions'
|
||||
import { AuthActions } from './actions/AuthActions'
|
||||
import { cached } from './util'
|
||||
|
||||
@@ -25,4 +25,9 @@ export class DataActions {
|
||||
get project () {
|
||||
return new ProjectActions(this.ctx)
|
||||
}
|
||||
|
||||
@cached
|
||||
get storybook () {
|
||||
return new StorybookActions(this.ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
WizardDataSource,
|
||||
BrowserDataSource,
|
||||
UtilDataSource,
|
||||
StorybookDataSource,
|
||||
} from './sources/'
|
||||
import { cached } from './util/cached'
|
||||
|
||||
@@ -104,6 +105,11 @@ export class DataContext {
|
||||
return new WizardDataSource(this)
|
||||
}
|
||||
|
||||
@cached
|
||||
get storybook () {
|
||||
return new StorybookDataSource(this)
|
||||
}
|
||||
|
||||
get wizardData () {
|
||||
return this.coreData.wizard
|
||||
}
|
||||
|
||||
@@ -185,4 +185,18 @@ export class ProjectActions {
|
||||
async clearLatestProjectCache () {
|
||||
await this.api.clearLatestProjectsCache()
|
||||
}
|
||||
|
||||
createComponentIndexHtml (template: string) {
|
||||
const project = this.ctx.activeProject
|
||||
|
||||
if (!project) {
|
||||
throw Error(`Cannot create index.html without activeProject.`)
|
||||
}
|
||||
|
||||
if (this.ctx.activeProject?.isFirstTimeCT) {
|
||||
const indexHtmlPath = path.resolve(this.ctx.activeProject.projectRoot, 'cypress/component/support/index.html')
|
||||
|
||||
this.ctx.fs.outputFile(indexHtmlPath, template)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import type { FoundSpec, FullConfig } from '@packages/types'
|
||||
import { CsfFile, readCsfOrMdx } from '@storybook/csf-tools'
|
||||
import endent from 'endent'
|
||||
import * as path from 'path'
|
||||
import type { DataContext } from '..'
|
||||
|
||||
export class StorybookActions {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
async generateSpecFromStory (storyPath: string) {
|
||||
const project = this.ctx.activeProject
|
||||
|
||||
if (!project) {
|
||||
throw Error(`Cannot generate a spec without activeProject.`)
|
||||
}
|
||||
|
||||
const config = await this.ctx.project.getConfig(project.projectRoot)
|
||||
|
||||
const spec = await this.generate(storyPath, project.projectRoot, config.componentFolder)
|
||||
|
||||
this.ctx.wizardData.generatedSpec = spec
|
||||
}
|
||||
|
||||
private async generate (
|
||||
storyPath: string,
|
||||
projectRoot: string,
|
||||
componentFolder: FullConfig['componentFolder'],
|
||||
): Promise<FoundSpec | null> {
|
||||
const storyFile = path.parse(storyPath)
|
||||
const storyName = storyFile.name.split('.')[0]
|
||||
|
||||
try {
|
||||
const raw = await readCsfOrMdx(storyPath, {
|
||||
defaultTitle: storyName || '',
|
||||
})
|
||||
const parsed = raw.parse()
|
||||
|
||||
if (
|
||||
(!parsed.meta.title && !parsed.meta.component) ||
|
||||
!parsed.stories.length
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const specFileExtension = '.cy-spec'
|
||||
const newSpecContent = this.generateSpecFromCsf(parsed, storyFile)
|
||||
const newSpecPath = path.join(
|
||||
storyPath,
|
||||
'..',
|
||||
`${parsed.meta.component}${specFileExtension}${storyFile.ext}`,
|
||||
)
|
||||
|
||||
// If this passes then the file exists and we don't want to overwrite it
|
||||
try {
|
||||
await this.ctx.fs.access(newSpecPath, this.ctx.fs.constants.F_OK)
|
||||
|
||||
return null
|
||||
} catch (e) {
|
||||
// eslint-disable-line no-empty
|
||||
}
|
||||
|
||||
await this.ctx.fs.outputFileSync(newSpecPath, newSpecContent)
|
||||
|
||||
const parsedSpec = path.parse(newSpecPath)
|
||||
|
||||
// Can this be obtained from the spec watcher?
|
||||
return {
|
||||
baseName: parsedSpec.base,
|
||||
fileName: parsedSpec.base.replace(specFileExtension, ''),
|
||||
specFileExtension,
|
||||
fileExtension: parsedSpec.ext,
|
||||
name: path.relative(componentFolder || projectRoot, newSpecPath),
|
||||
relative: path.relative(projectRoot, newSpecPath),
|
||||
absolute: newSpecPath,
|
||||
specType: 'component',
|
||||
}
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private generateSpecFromCsf (parsed: CsfFile, storyFile: path.ParsedPath) {
|
||||
const isReact = parsed._ast.program.body.some(
|
||||
(statement) => {
|
||||
return statement.type === 'ImportDeclaration' &&
|
||||
statement.source.value === 'react'
|
||||
},
|
||||
)
|
||||
const isVue = parsed._ast.program.body.some(
|
||||
(statement) => {
|
||||
return statement.type === 'ImportDeclaration' &&
|
||||
statement.source.value.endsWith('.vue')
|
||||
},
|
||||
)
|
||||
|
||||
if (!isReact && !isVue) {
|
||||
throw new Error('Provided story is not supported')
|
||||
}
|
||||
|
||||
const getDependency = () => {
|
||||
return endent`
|
||||
import { mount } from "@cypress/${isReact ? 'react' : 'vue'}"
|
||||
import { composeStories } from "@storybook/testing-${isReact ? 'react' : 'vue3'}"`
|
||||
}
|
||||
const getMountSyntax = (component: string) => {
|
||||
return isReact ? `<${component} />` : `${component}()`
|
||||
}
|
||||
const itContent = parsed.stories
|
||||
.map((story, i) => {
|
||||
const component = story.name.replace(/\s+/, '')
|
||||
let it = endent`
|
||||
it('should render ${component}', () => {
|
||||
const { ${component} } = composedStories
|
||||
mount(${getMountSyntax(component)})
|
||||
})`
|
||||
|
||||
if (i !== 0) {
|
||||
it = it
|
||||
.split('\n')
|
||||
.map((line) => `// ${line}`)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
return it
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
return endent`${isReact ? `import React from "react"` : ''}
|
||||
import * as stories from "./${storyFile.name}"
|
||||
${getDependency()}
|
||||
|
||||
const composedStories = composeStories(stories)
|
||||
|
||||
describe('${parsed.meta.title || parsed.meta.component}', () => {
|
||||
${itContent}
|
||||
})`
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ export * from './AppActions'
|
||||
export * from './AuthActions'
|
||||
export * from './FileActions'
|
||||
export * from './ProjectActions'
|
||||
export * from './StorybookActions'
|
||||
export * from './WizardActions'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BUNDLERS, FoundBrowser, FoundSpec } from '@packages/types'
|
||||
import { BUNDLERS, FoundBrowser, FoundSpec, StorybookFile } from '@packages/types'
|
||||
import type { NexusGenEnums } from '@packages/graphql/src/gen/nxs.gen'
|
||||
|
||||
export type Maybe<T> = T | null | undefined
|
||||
@@ -39,6 +39,7 @@ export interface WizardDataShape {
|
||||
chosenFramework: NexusGenEnums['FrontendFrameworkEnum'] | null
|
||||
chosenManualInstall: boolean
|
||||
chosenBrowser: FoundBrowser | null
|
||||
generatedSpec: Omit<StorybookFile, 'content'> | null
|
||||
}
|
||||
|
||||
export interface CoreDataShape {
|
||||
@@ -68,6 +69,7 @@ export function makeCoreData (): CoreDataShape {
|
||||
allBundlers: BUNDLERS,
|
||||
history: ['welcome'],
|
||||
chosenBrowser: null,
|
||||
generatedSpec: null,
|
||||
},
|
||||
user: null,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import glob from 'glob'
|
||||
import * as path from 'path'
|
||||
import type { DataContext } from '..'
|
||||
import { promisify } from 'util'
|
||||
import type { StorybookInfo } from '@packages/types'
|
||||
|
||||
const STORYBOOK_FILES = [
|
||||
'main.js',
|
||||
'preview.js',
|
||||
'preview-head.html',
|
||||
'preview-body.html',
|
||||
]
|
||||
|
||||
export class StorybookDataSource {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
get storybookInfo () {
|
||||
if (!this.ctx.activeProject?.projectRoot) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
return this.storybookInfoLoader.load(this.ctx.activeProject?.projectRoot)
|
||||
}
|
||||
|
||||
private storybookInfoLoader = this.ctx.loader<string, StorybookInfo | null>((projectRoots) => this.batchStorybookInfo(projectRoots))
|
||||
|
||||
private batchStorybookInfo (projectRoots: readonly string[]) {
|
||||
return Promise.all(projectRoots.map((projectRoot) => this.detectStorybook(projectRoot)))
|
||||
}
|
||||
|
||||
private async detectStorybook (projectRoot: string): Promise<StorybookInfo | null> {
|
||||
const storybookRoot = path.join(projectRoot, '.storybook')
|
||||
const storybookInfo: StorybookInfo = {
|
||||
storybookRoot,
|
||||
files: [],
|
||||
storyGlobs: [],
|
||||
getStories: this.getStories,
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ctx.fs.access(storybookRoot, this.ctx.fs.constants.F_OK)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const fileName of STORYBOOK_FILES) {
|
||||
try {
|
||||
const absolute = path.join(storybookRoot, fileName)
|
||||
const file = {
|
||||
name: fileName,
|
||||
relative: path.relative(projectRoot, absolute),
|
||||
absolute,
|
||||
content: await this.ctx.fs.readFile(absolute, 'utf-8'),
|
||||
}
|
||||
|
||||
storybookInfo.files.push(file)
|
||||
} catch (e) {
|
||||
// eslint-disable-line no-empty
|
||||
}
|
||||
}
|
||||
|
||||
const mainJs = storybookInfo.files.find(({ name }) => name === 'main.js')
|
||||
|
||||
if (mainJs) {
|
||||
try {
|
||||
// Does this need to be wrapped in IPC?
|
||||
const mainJsModule = require(mainJs.absolute)
|
||||
|
||||
storybookInfo.storyGlobs = mainJsModule.stories
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
return storybookInfo
|
||||
}
|
||||
|
||||
private async getStories (storybookRoot: string, storyGlobs: string[]) {
|
||||
const files: string[] = []
|
||||
|
||||
for (const storyPattern of storyGlobs) {
|
||||
const res = await promisify(glob)(path.join(storybookRoot, storyPattern))
|
||||
|
||||
files.push(...res)
|
||||
}
|
||||
|
||||
// Don't currently support mdx
|
||||
return files.filter((file) => !file.endsWith('.mdx'))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Bundler, BUNDLERS, FrontendFramework, FRONTEND_FRAMEWORKS, PACKAGES_DESCRIPTIONS, WIZARD_STEPS } from '@packages/types'
|
||||
import { Bundler, BUNDLERS, FrontendFramework, FRONTEND_FRAMEWORKS, PACKAGES_DESCRIPTIONS, StorybookInfo, WIZARD_STEPS } from '@packages/types'
|
||||
import dedent from 'dedent'
|
||||
import endent from 'endent'
|
||||
import type { NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { DataContext } from '..'
|
||||
|
||||
@@ -74,8 +75,9 @@ export class WizardDataSource {
|
||||
return true
|
||||
}
|
||||
|
||||
sampleCode (lang: 'js' | 'ts') {
|
||||
async sampleCode (lang: 'js' | 'ts') {
|
||||
const data = this.ctx.wizardData
|
||||
const storybookInfo = await this.ctx.storybook.storybookInfo
|
||||
|
||||
if (data.chosenTestingType === 'component') {
|
||||
if (!this.chosenFramework || !this.chosenBundler) {
|
||||
@@ -87,6 +89,7 @@ export class WizardDataSource {
|
||||
framework: this.chosenFramework,
|
||||
bundler: this.chosenBundler,
|
||||
lang,
|
||||
storybookInfo,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -100,6 +103,20 @@ export class WizardDataSource {
|
||||
return null
|
||||
}
|
||||
|
||||
async sampleTemplate () {
|
||||
const storybookInfo = await this.ctx.storybook.storybookInfo
|
||||
|
||||
if (!this.chosenFramework || !this.chosenBundler) {
|
||||
return null
|
||||
}
|
||||
|
||||
return wizardGetComponentIndexHtml({
|
||||
framework: this.chosenFramework,
|
||||
bundler: this.chosenBundler,
|
||||
storybookInfo,
|
||||
})
|
||||
}
|
||||
|
||||
get chosenTestingType () {
|
||||
return this.ctx.wizardData.chosenTestingType
|
||||
}
|
||||
@@ -125,6 +142,7 @@ interface GetCodeOptsCt {
|
||||
framework: FrontendFramework
|
||||
bundler: Bundler
|
||||
lang: WizardCodeLanguage
|
||||
storybookInfo?: StorybookInfo | null
|
||||
}
|
||||
|
||||
type GetCodeOpts = GetCodeOptsCt | GetCodeOptsE2E
|
||||
@@ -162,7 +180,7 @@ const wizardGetConfigCodeCt = (opts: GetCodeOptsCt): string | null => {
|
||||
const { framework, bundler, lang } = opts
|
||||
|
||||
const comments = `Component testing, ${LanguageNames[opts.lang]}, ${framework.name}, ${bundler.name}`
|
||||
const frameworkConfig = FRAMEWORK_CONFIG_FILE[framework.type]
|
||||
const frameworkConfig = getFrameworkConfigFile(opts)
|
||||
|
||||
if (frameworkConfig) {
|
||||
return `// ${comments}
|
||||
@@ -204,63 +222,144 @@ ${exportStatement}
|
||||
}`
|
||||
}
|
||||
|
||||
const FRAMEWORK_CONFIG_FILE: Partial<Record<NexusGenEnums['FrontendFrameworkEnum'], Record<NexusGenEnums['WizardCodeLanguage'], string> | null>> = {
|
||||
nextjs: {
|
||||
js: dedent`
|
||||
const injectNextDevServer = require('@cypress/react/plugins/next')
|
||||
const getFrameworkConfigFile = (opts: GetCodeOptsCt) => {
|
||||
return {
|
||||
nextjs: {
|
||||
js: dedent`
|
||||
const injectNextDevServer = require('@cypress/react/plugins/next')
|
||||
|
||||
module.exports = {
|
||||
component (on, config) {
|
||||
injectNextDevServer(on, config)
|
||||
},
|
||||
}
|
||||
`,
|
||||
ts: dedent`
|
||||
import { defineConfig } from 'cypress'
|
||||
import injectNextDevServer from '@cypress/react/plugins/next'
|
||||
module.exports = {
|
||||
component (on, config) {
|
||||
injectNextDevServer(on, config)
|
||||
},
|
||||
}
|
||||
`,
|
||||
ts: dedent`
|
||||
import { defineConfig } from 'cypress'
|
||||
import injectNextDevServer from '@cypress/react/plugins/next'
|
||||
|
||||
export default defineConfig({
|
||||
component (on, config) {
|
||||
injectNextDevServer(on, config)
|
||||
},
|
||||
})
|
||||
`,
|
||||
},
|
||||
nuxtjs: {
|
||||
js: dedent`
|
||||
const { startDevServer } = require('@cypress/webpack-dev-server')
|
||||
const { getWebpackConfig } = require('nuxt')
|
||||
export default defineConfig({
|
||||
component (on, config) {
|
||||
injectNextDevServer(on, config)
|
||||
},
|
||||
})
|
||||
`,
|
||||
},
|
||||
nuxtjs: {
|
||||
js: dedent`
|
||||
const { startDevServer } = require('@cypress/webpack-dev-server')
|
||||
const { getWebpackConfig } = require('nuxt')
|
||||
|
||||
module.exports = {
|
||||
component (on, config) {
|
||||
on('dev-server:start', async (options) => {
|
||||
let webpackConfig = await getWebpackConfig('modern', 'dev')
|
||||
module.exports = {
|
||||
component (on, config) {
|
||||
on('dev-server:start', async (options) => {
|
||||
let webpackConfig = await getWebpackConfig('modern', 'dev')
|
||||
|
||||
return startDevServer({
|
||||
options,
|
||||
webpackConfig,
|
||||
return startDevServer({
|
||||
options,
|
||||
webpackConfig,
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
`,
|
||||
ts: dedent`
|
||||
import { defineConfig } from 'cypress'
|
||||
import { startDevServer } from '@cypress/webpack-dev-server'
|
||||
import { getWebpackConfig } from 'nuxt'
|
||||
},
|
||||
}
|
||||
`,
|
||||
ts: dedent`
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
component (on, config) {
|
||||
on('dev-server:start', async (options) => {
|
||||
let webpackConfig = await getWebpackConfig('modern', 'dev')
|
||||
export default defineConfig({
|
||||
component: {
|
||||
testFiles: "**/*cy-spec.tsx",
|
||||
componentFolder: "src"
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
cra: {
|
||||
js: endent`
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
return startDevServer({
|
||||
options,
|
||||
webpackConfig,
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
`,
|
||||
},
|
||||
module.exports = defineConfig({
|
||||
component: {
|
||||
testFiles: "**/*cy-spec.{js,jsx,ts,tsx}",
|
||||
componentFolder: "src"
|
||||
}
|
||||
})
|
||||
`,
|
||||
ts: endent`
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
testFiles: "**/*cy-spec.{js,jsx,ts,tsx}",
|
||||
componentFolder: "src"
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
vuecli: {
|
||||
js: endent`
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
component: {
|
||||
testFiles: "**/*cy-spec.{js,jsx,ts,tsx}",
|
||||
componentFolder: "src"
|
||||
}
|
||||
})
|
||||
`,
|
||||
ts: endent`
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
testFiles: "**/*cy-spec.{js,jsx,ts,tsx}",
|
||||
componentFolder: "src"
|
||||
}
|
||||
})
|
||||
`,
|
||||
},
|
||||
}[opts.framework.type as string]
|
||||
}
|
||||
|
||||
export const wizardGetComponentIndexHtml = (opts: Omit<GetCodeOptsCt, 'lang' | 'type'>) => {
|
||||
const framework = opts.framework.type
|
||||
let headModifier = ''
|
||||
let bodyModifier = ''
|
||||
|
||||
if (framework === 'nextjs') {
|
||||
headModifier += '<div id="__next_css__DO_NOT_USE__"></div>'
|
||||
}
|
||||
|
||||
const previewHead = opts.storybookInfo?.files.find(({ name }) => name === 'preview-head.html')
|
||||
|
||||
if (previewHead) {
|
||||
headModifier += previewHead.content
|
||||
}
|
||||
|
||||
const previewBody = opts.storybookInfo?.files.find(({ name }) => name === 'preview-body.html')
|
||||
|
||||
if (previewBody) {
|
||||
headModifier += previewBody.content
|
||||
}
|
||||
|
||||
return getComponentTemplate({ headModifier, bodyModifier })
|
||||
}
|
||||
|
||||
const getComponentTemplate = (opts: {headModifier: string, bodyModifier: string}) => {
|
||||
// TODO: Properly indent additions and strip newline if none
|
||||
return endent`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
${opts.headModifier}
|
||||
</head>
|
||||
<body>
|
||||
${opts.bodyModifier}
|
||||
<div id="__cy_root"></div>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ export * from './BrowserDataSource'
|
||||
export * from './FileDataSource'
|
||||
export * from './GitDataSource'
|
||||
export * from './ProjectDataSource'
|
||||
export * from './StorybookDataSource'
|
||||
export * from './UtilDataSource'
|
||||
export * from './WizardDataSource'
|
||||
|
||||
@@ -40,4 +40,7 @@ export const stubMutation: MaybeResolver<Mutation> = {
|
||||
|
||||
return ctx.app
|
||||
},
|
||||
generateSpecFromStory (source, args, ctx) {
|
||||
return ctx.wizard
|
||||
},
|
||||
}
|
||||
|
||||
@@ -269,10 +269,16 @@ type Mutation {
|
||||
path: String!
|
||||
): App!
|
||||
|
||||
"""Create an Index HTML file for a new component testing project"""
|
||||
appCreateComponentIndexHtml(template: String!): App
|
||||
|
||||
"""Create a Cypress config file for a new project"""
|
||||
appCreateConfigFile(code: String!, configFilename: String!): App
|
||||
clearActiveProject: Query!
|
||||
|
||||
"""Generate spec from Storybook story"""
|
||||
generateSpecFromStory(storyPath: String!): Wizard!
|
||||
|
||||
"""
|
||||
Initializes open_project global singleton to manager current project state
|
||||
"""
|
||||
@@ -521,6 +527,18 @@ enum SpecType {
|
||||
integration
|
||||
}
|
||||
|
||||
"""Storybook"""
|
||||
type Storybook {
|
||||
"""Whether this is the selected framework bundler"""
|
||||
configured: Boolean
|
||||
|
||||
"""Most recent generated spec"""
|
||||
generatedSpec: String
|
||||
|
||||
"""List of all Storybook stories"""
|
||||
stories: [String!]!
|
||||
}
|
||||
|
||||
"""The bundlers that we can use with Cypress"""
|
||||
enum SupportedBundlers {
|
||||
vite
|
||||
@@ -574,7 +592,11 @@ type Wizard {
|
||||
|
||||
"""Configuration file based on bundler and framework of choice"""
|
||||
sampleCode(lang: WizardCodeLanguage! = js): String
|
||||
|
||||
"""IndexHtml file based on bundler and framework of choice"""
|
||||
sampleTemplate: String
|
||||
step: WizardStep!
|
||||
storybook: Storybook
|
||||
|
||||
"""
|
||||
The testing type we are setting in the wizard, null if this has not been chosen
|
||||
|
||||
@@ -105,6 +105,32 @@ export const mutation = mutationType({
|
||||
},
|
||||
})
|
||||
|
||||
t.field('appCreateComponentIndexHtml', {
|
||||
type: 'App',
|
||||
args: {
|
||||
template: nonNull('String'),
|
||||
},
|
||||
description: 'Create an Index HTML file for a new component testing project',
|
||||
resolve: async (root, args, ctx) => {
|
||||
await ctx.actions.project.createComponentIndexHtml(args.template)
|
||||
|
||||
return ctx.appData
|
||||
},
|
||||
})
|
||||
|
||||
t.nonNull.field('generateSpecFromStory', {
|
||||
type: 'Wizard',
|
||||
description: 'Generate spec from Storybook story',
|
||||
args: {
|
||||
storyPath: nonNull('String'),
|
||||
},
|
||||
async resolve (_root, args, ctx) {
|
||||
await ctx.actions.storybook.generateSpecFromStory(args.storyPath)
|
||||
|
||||
return ctx.wizardData
|
||||
},
|
||||
})
|
||||
|
||||
t.field('navigationMenuSetItem', {
|
||||
type: 'NavigationMenu',
|
||||
description: 'Set the current navigation item',
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { objectType } from 'nexus'
|
||||
|
||||
export const Storybook = objectType({
|
||||
name: 'Storybook',
|
||||
description: 'Storybook',
|
||||
definition (t) {
|
||||
t.boolean('configured', {
|
||||
description: 'Whether this is the selected framework bundler',
|
||||
resolve: async (source, args, ctx) => !!(await ctx.storybook.storybookInfo),
|
||||
})
|
||||
|
||||
t.nonNull.list.nonNull.string('stories', {
|
||||
description: 'List of all Storybook stories',
|
||||
resolve: async (source, args, ctx) => {
|
||||
const storybook = await ctx.storybook.storybookInfo
|
||||
|
||||
if (!storybook) {
|
||||
return []
|
||||
}
|
||||
|
||||
return storybook.getStories(storybook.storybookRoot, storybook.storyGlobs)
|
||||
},
|
||||
})
|
||||
|
||||
t.string('generatedSpec', {
|
||||
description: 'Most recent generated spec',
|
||||
resolve: (source, args, ctx) => ctx.wizardData.generatedSpec?.relative ?? null,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import { WizardNpmPackage } from './gql-WizardNpmPackage'
|
||||
import { arg, nonNull, objectType } from 'nexus'
|
||||
import { BUNDLERS, FRONTEND_FRAMEWORKS, TESTING_TYPES } from '@packages/types'
|
||||
import { TestingTypeEnum, WizardCodeLanguageEnum, WizardStepEnum } from '../enumTypes/gql-WizardEnums'
|
||||
import { Storybook } from './gql-Storybook'
|
||||
|
||||
export const Wizard = objectType({
|
||||
name: 'Wizard',
|
||||
@@ -71,6 +72,11 @@ export const Wizard = objectType({
|
||||
resolve: (source, args, ctx) => ctx.wizard.sampleCode(args.lang),
|
||||
})
|
||||
|
||||
t.string('sampleTemplate', {
|
||||
description: 'IndexHtml file based on bundler and framework of choice',
|
||||
resolve: (source, args, ctx) => ctx.wizard.sampleTemplate(),
|
||||
})
|
||||
|
||||
t.nonNull.field('step', {
|
||||
type: WizardStepEnum,
|
||||
resolve: (source) => source.currentStep,
|
||||
@@ -91,6 +97,11 @@ export const Wizard = objectType({
|
||||
description: 'The title of the page, given the current step of the wizard',
|
||||
resolve: (source, args, ctx) => ctx.wizard.title ?? null,
|
||||
})
|
||||
|
||||
t.field('storybook', {
|
||||
type: Storybook,
|
||||
resolve: (source, args, ctx) => ctx.storybook.storybookInfo,
|
||||
})
|
||||
},
|
||||
sourceType: {
|
||||
module: '@packages/data-context/src/data/coreDataShape',
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from './gql-NavigationMenu'
|
||||
export * from './gql-Project'
|
||||
export * from './gql-Query'
|
||||
export * from './gql-Spec'
|
||||
export * from './gql-Storybook'
|
||||
export * from './gql-TestingTypeInfo'
|
||||
export * from './gql-Wizard'
|
||||
export * from './gql-WizardBundler'
|
||||
|
||||
@@ -73,7 +73,7 @@ import PrismJs from 'vue-prism-component'
|
||||
import WizardLayout from './WizardLayout.vue'
|
||||
import CopyButton from '@cy/components/CopyButton.vue'
|
||||
import { languages } from '../utils/configFile'
|
||||
import { ConfigFileFragment, ConfigFile_AppCreateConfigFileDocument } from '../generated/graphql'
|
||||
import { ConfigFileFragment, ConfigFile_AppCreateConfigFileDocument, ConfigFile_AppCreateComponentIndexHtmlDocument } from '../generated/graphql'
|
||||
import { useMutation } from '@urql/vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
|
||||
@@ -98,6 +98,7 @@ fragment SampleCode on Wizard {
|
||||
canNavigateForward
|
||||
sampleCodeJs: sampleCode(lang: js)
|
||||
sampleCodeTs: sampleCode(lang: ts)
|
||||
sampleTemplate
|
||||
}
|
||||
`
|
||||
|
||||
@@ -112,6 +113,17 @@ mutation ConfigFile_appCreateConfigFile($code: String!, $configFilename: String!
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation ConfigFile_appCreateComponentIndexHtml($template: String!) {
|
||||
appCreateComponentIndexHtml(template: $template) {
|
||||
activeProject {
|
||||
id
|
||||
projectRoot
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const props = defineProps<{
|
||||
gql: ConfigFileFragment
|
||||
}>()
|
||||
@@ -133,6 +145,7 @@ import('prismjs/components/prism-typescript').then(() => {
|
||||
})
|
||||
|
||||
const createConfigFile = useMutation(ConfigFile_AppCreateConfigFileDocument)
|
||||
const createComponentIndexHtml = useMutation(ConfigFile_AppCreateComponentIndexHtmlDocument)
|
||||
|
||||
const code = computed(() => {
|
||||
if (language.value === 'js') {
|
||||
@@ -160,6 +173,10 @@ const createConfig = async () => {
|
||||
code: code.value,
|
||||
configFilename: `cypress.config.${language.value}`,
|
||||
})
|
||||
|
||||
await createComponentIndexHtml.executeMutation({
|
||||
template: props.gql.wizard.sampleTemplate as string,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,3 +24,5 @@ export {
|
||||
} from './config'
|
||||
|
||||
export * from './server'
|
||||
|
||||
export * from './storybook'
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface StorybookFile {
|
||||
name: string
|
||||
absolute: string
|
||||
relative: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface StorybookInfo {
|
||||
storybookRoot: string
|
||||
files: StorybookFile[]
|
||||
storyGlobs: string[]
|
||||
getStories: (
|
||||
storybookRoot: string,
|
||||
storyGlobs: string[]
|
||||
) => Promise<string[]>
|
||||
}
|
||||
@@ -2081,7 +2081,7 @@
|
||||
"@babel/parser" "^7.15.4"
|
||||
"@babel/types" "^7.15.4"
|
||||
|
||||
"@babel/traverse@7.15.4", "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.15.4", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.6.0", "@babel/traverse@^7.7.0", "@babel/traverse@^7.8.3", "@babel/traverse@^7.9.0":
|
||||
"@babel/traverse@7.15.4", "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.15.4", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.6.0", "@babel/traverse@^7.7.0", "@babel/traverse@^7.8.3", "@babel/traverse@^7.9.0":
|
||||
version "7.15.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d"
|
||||
integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==
|
||||
@@ -7764,6 +7764,29 @@
|
||||
"@storybook/core-client" "6.2.7"
|
||||
"@storybook/core-server" "6.2.7"
|
||||
|
||||
"@storybook/csf-tools@^6.4.0-alpha.38":
|
||||
version "6.4.0-beta.4"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.4.0-beta.4.tgz#87e7afd9050c4b09755106bc5cb3fc31e9a1b271"
|
||||
integrity sha512-CctTaXCpmfKQHVXuAyXcHoUXjDYfXTbNGLZkQ3jnceGvf/oMqp7bqennpM7CnnHayk0sOnlppAwnQDABb2RCog==
|
||||
dependencies:
|
||||
"@babel/core" "^7.12.10"
|
||||
"@babel/generator" "^7.12.11"
|
||||
"@babel/parser" "^7.12.11"
|
||||
"@babel/plugin-transform-react-jsx" "^7.12.12"
|
||||
"@babel/preset-env" "^7.12.11"
|
||||
"@babel/traverse" "^7.12.11"
|
||||
"@babel/types" "^7.12.11"
|
||||
"@mdx-js/mdx" "^1.6.22"
|
||||
"@storybook/csf" "0.0.2--canary.6aca495.0"
|
||||
core-js "^3.8.2"
|
||||
fs-extra "^9.0.1"
|
||||
global "^4.4.0"
|
||||
js-string-escape "^1.0.1"
|
||||
lodash "^4.17.20"
|
||||
prettier "^2.2.1"
|
||||
regenerator-runtime "^0.13.7"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/csf@0.0.1":
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.1.tgz#95901507dc02f0bc6f9ac8ee1983e2fc5bb98ce6"
|
||||
@@ -7771,6 +7794,13 @@
|
||||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
"@storybook/csf@0.0.2--canary.6aca495.0":
|
||||
version "0.0.2--canary.6aca495.0"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.2--canary.6aca495.0.tgz#1569819337b0414e6e5575680c96505eda803d0b"
|
||||
integrity sha512-SRKP/zXfHQ/tAlqVAysXGyVrYvILwyDuhisXP6hpXW7BHej2BXVxR4LizA5AFpj99j066KbxdHKFMNmrncZEuw==
|
||||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
"@storybook/node-logger@6.2.7":
|
||||
version "6.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.2.7.tgz#2329042e44eba1efcc6bb0689c8566a555d5b0ee"
|
||||
@@ -33050,6 +33080,11 @@ prettier@^1.16.4, prettier@^1.18.2:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
|
||||
prettier@^2.2.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
|
||||
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
|
||||
|
||||
prettier@~2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
|
||||
|
||||
Reference in New Issue
Block a user