Skip to content

Commit 344c9b4

Browse files
committed
feat: create a copy when selecting a pre-existing story
1 parent 9a19ad8 commit 344c9b4

File tree

2 files changed

+100
-60
lines changed

2 files changed

+100
-60
lines changed
Lines changed: 96 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { FoundSpec, FullConfig } from '@packages/types'
2-
import { CsfFile, readCsfOrMdx } from '@storybook/csf-tools'
2+
import { readCsfOrMdx } from '@storybook/csf-tools'
33
import endent from 'endent'
44
import * as path from 'path'
55
import type { DataContext } from '..'
@@ -16,82 +16,95 @@ export class StorybookActions {
1616

1717
const config = await this.ctx.project.getConfig(project.projectRoot)
1818

19-
const spec = await this.generate(storyPath, project.projectRoot, config.componentFolder)
19+
const spec = await this.generateSpec(
20+
storyPath,
21+
project.projectRoot,
22+
config.componentFolder,
23+
)
2024

2125
this.ctx.wizardData.generatedSpec = spec
2226
}
2327

24-
private async generate (
28+
private async generateSpec (
2529
storyPath: string,
2630
projectRoot: string,
2731
componentFolder: FullConfig['componentFolder'],
2832
): Promise<FoundSpec | null> {
29-
const storyFile = path.parse(storyPath)
30-
const storyName = storyFile.name.split('.')[0]
33+
const specFileExtension = '.cy'
34+
const parsedFile = path.parse(storyPath)
35+
const fileName = parsedFile.name.split('.')[0] as string
3136

32-
try {
33-
const raw = await readCsfOrMdx(storyPath, {
34-
defaultTitle: storyName || '',
35-
})
36-
const parsed = raw.parse()
37-
38-
if (
39-
(!parsed.meta.title && !parsed.meta.component) ||
40-
!parsed.stories.length
41-
) {
42-
return null
43-
}
37+
let newSpecContent: string | null
4438

45-
const specFileExtension = '.cy-spec'
46-
const newSpecContent = this.generateSpecFromCsf(parsed, storyFile)
47-
const newSpecPath = path.join(
39+
try {
40+
newSpecContent = await this.generateSpecFromCsf(
4841
storyPath,
49-
'..',
50-
`${parsed.meta.component}${specFileExtension}${storyFile.ext}`,
42+
fileName,
5143
)
5244

53-
// If this passes then the file exists and we don't want to overwrite it
54-
try {
55-
await this.ctx.fs.access(newSpecPath, this.ctx.fs.constants.F_OK)
56-
45+
if (!newSpecContent) {
5746
return null
58-
} catch (e) {
59-
// eslint-disable-line no-empty
6047
}
48+
} catch (e) {
49+
return null
50+
}
6151

62-
await this.ctx.fs.outputFileSync(newSpecPath, newSpecContent)
63-
64-
const parsedSpec = path.parse(newSpecPath)
52+
let newSpecAbsolute = path.join(
53+
parsedFile.dir,
54+
`${fileName}${specFileExtension}${parsedFile.ext}`,
55+
)
6556

66-
// Can this be obtained from the spec watcher?
67-
return {
68-
baseName: parsedSpec.base,
69-
fileName: parsedSpec.base.replace(specFileExtension, ''),
57+
try {
58+
newSpecAbsolute = await this.getFilename(
59+
newSpecAbsolute,
60+
fileName,
7061
specFileExtension,
71-
fileExtension: parsedSpec.ext,
72-
name: path.relative(componentFolder || projectRoot, newSpecPath),
73-
relative: path.relative(projectRoot, newSpecPath),
74-
absolute: newSpecPath,
75-
specType: 'component',
76-
}
62+
)
63+
64+
await this.ctx.fs.outputFileSync(newSpecAbsolute, newSpecContent)
7765
} catch (e) {
7866
return null
7967
}
68+
69+
const parsedNewSpec = path.parse(newSpecAbsolute)
70+
71+
// Can this be obtained from the spec watcher?
72+
return {
73+
absolute: newSpecAbsolute,
74+
baseName: parsedNewSpec.base,
75+
fileExtension: parsedNewSpec.ext,
76+
fileName,
77+
name: path.relative(componentFolder || projectRoot, newSpecAbsolute),
78+
relative: path.relative(projectRoot, newSpecAbsolute),
79+
specFileExtension,
80+
specType: 'component',
81+
}
8082
}
8183

82-
private generateSpecFromCsf (parsed: CsfFile, storyFile: path.ParsedPath) {
83-
const isReact = parsed._ast.program.body.some(
84-
(statement) => {
85-
return statement.type === 'ImportDeclaration' &&
84+
private async generateSpecFromCsf (
85+
absolute: string,
86+
storyName: string,
87+
): Promise<null | string> {
88+
const csf = await readCsfOrMdx(absolute, {
89+
defaultTitle: storyName,
90+
}).then((res) => res.parse())
91+
92+
if ((!csf.meta.title && !csf.meta.component) || !csf.stories.length) {
93+
return null
94+
}
95+
96+
const isReact = csf._ast.program.body.some((statement) => {
97+
return (
98+
statement.type === 'ImportDeclaration' &&
8699
statement.source.value === 'react'
87-
},
88-
)
89-
const isVue = parsed._ast.program.body.some(
90-
(statement) => {
91-
return statement.type === 'ImportDeclaration' &&
100+
)
101+
})
102+
const isVue = csf._ast.program.body.some((statement) => {
103+
return (
104+
statement.type === 'ImportDeclaration' &&
92105
statement.source.value.endsWith('.vue')
93-
},
94-
)
106+
)
107+
})
95108

96109
if (!isReact && !isVue) {
97110
throw new Error('Provided story is not supported')
@@ -100,12 +113,14 @@ export class StorybookActions {
100113
const getDependency = () => {
101114
return endent`
102115
import { mount } from "@cypress/${isReact ? 'react' : 'vue'}"
103-
import { composeStories } from "@storybook/testing-${isReact ? 'react' : 'vue3'}"`
116+
import { composeStories } from "@storybook/testing-${
117+
isReact ? 'react' : 'vue3'
118+
}"`
104119
}
105120
const getMountSyntax = (component: string) => {
106121
return isReact ? `<${component} />` : `${component}()`
107122
}
108-
const itContent = parsed.stories
123+
const itContent = csf.stories
109124
.map((story, i) => {
110125
const component = story.name.replace(/\s+/, '')
111126
let it = endent`
@@ -126,13 +141,38 @@ export class StorybookActions {
126141
.join('\n\n')
127142

128143
return endent`${isReact ? `import React from "react"` : ''}
129-
import * as stories from "./${storyFile.name}"
144+
import * as stories from "./${path.parse(absolute).name}"
130145
${getDependency()}
131146
132147
const composedStories = composeStories(stories)
133148
134-
describe('${parsed.meta.title || parsed.meta.component}', () => {
149+
describe('${csf.meta.title || csf.meta.component}', () => {
135150
${itContent}
136151
})`
137152
}
153+
154+
private async getFilename (absolute: string, fileName: string, specFileExtension: string) {
155+
let fileToGenerate = absolute
156+
const { dir, ext } = path.parse(absolute)
157+
let i = 0
158+
159+
while (this.fileExists(fileToGenerate)) {
160+
fileToGenerate = path.join(
161+
dir,
162+
`${fileName}-copy-${++i}${specFileExtension}${ext}`,
163+
)
164+
}
165+
166+
return fileToGenerate
167+
}
168+
169+
private fileExists (absolute: string) {
170+
try {
171+
this.ctx.fs.accessSync(absolute, this.ctx.fs.constants.F_OK)
172+
173+
return true
174+
} catch (e) {
175+
return false
176+
}
177+
}
138178
}

packages/data-context/src/sources/WizardDataSource.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ const getFrameworkConfigFile = (opts: GetCodeOptsCt) => {
280280
281281
module.exports = defineConfig({
282282
component: {
283-
testFiles: "**/*cy-spec.{js,jsx,ts,tsx}",
283+
testFiles: "**/*.cy.{js,jsx,ts,tsx}",
284284
componentFolder: "src"
285285
}
286286
})
@@ -290,7 +290,7 @@ const getFrameworkConfigFile = (opts: GetCodeOptsCt) => {
290290
291291
export default defineConfig({
292292
component: {
293-
testFiles: "**/*cy-spec.{js,jsx,ts,tsx}",
293+
testFiles: "**/*.cy.{js,jsx,ts,tsx}",
294294
componentFolder: "src"
295295
}
296296
})
@@ -302,7 +302,7 @@ const getFrameworkConfigFile = (opts: GetCodeOptsCt) => {
302302
303303
module.exports = defineConfig({
304304
component: {
305-
testFiles: "**/*cy-spec.{js,jsx,ts,tsx}",
305+
testFiles: "**/*.cy.{js,jsx,ts,tsx}",
306306
componentFolder: "src"
307307
}
308308
})
@@ -312,7 +312,7 @@ const getFrameworkConfigFile = (opts: GetCodeOptsCt) => {
312312
313313
export default defineConfig({
314314
component: {
315-
testFiles: "**/*cy-spec.{js,jsx,ts,tsx}",
315+
testFiles: "**/*.cy.{js,jsx,ts,tsx}",
316316
componentFolder: "src"
317317
}
318318
})

0 commit comments

Comments
 (0)