diff --git a/README.md b/README.md index 38a9a6a..5c2f5dc 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,10 @@ For full documentation, please see the [Playwright testing docs on the official > This will resolve a build error related to `Symbol.asyncDispose`. If this is not added, tests may fail to run since the Stencil dev server will be unable > to start due to the build error. -1. Ensure the Stencil project has a [`www` output target](https://stenciljs.com/docs/www). Playwright relies on pre-compiled output running in a dev server +1. Ensure the Stencil project has a [`www`](https://stenciljs.com/docs/www), `dist`, or `loader-bundle` output target. Playwright relies on pre-compiled output running in a dev server to run tests against. When using the `createConfig()` helper, a configuration for the dev server will be automatically created based on - the Stencil project's `www` output target config and [dev server config](https://stenciljs.com/docs/dev-server). If no `www` output target is specified, - tests will not be able to run. + the Stencil project's output target config and [dev server config](https://stenciljs.com/docs/dev-server). The adapter will look for output targets in the following order of priority: `www` > `dist` (Stencil < v5) / `loader-bundle` (Stencil v5). If none of these output targets are specified, + tests may not be able to run. 1. Add the `copy` option to the `www` output target config: diff --git a/src/load-config-meta.ts b/src/load-config-meta.ts index ecfa82f..26d66ef 100644 --- a/src/load-config-meta.ts +++ b/src/load-config-meta.ts @@ -2,7 +2,17 @@ import { loadConfig, OutputTargetWww } from '@stencil/core/compiler'; import { findUp } from 'find-up'; import { existsSync } from 'fs'; -import { relative } from 'path'; +import { join, relative } from 'path'; + +/** + * Common shape for output targets with dir and buildDir properties. + * Used for dist (v4) and loader-bundle (v5) targets. + */ +interface OutputTargetWithDir { + type: string; + dir?: string; + buildDir?: string; +} const DEFAULT_NAMESPACE = 'app'; const DEFAULT_BASE_URL = 'http://localhost:3333'; @@ -33,24 +43,41 @@ export const loadConfigMeta = async (cwd?: string) => { if (stencilConfigPath && existsSync(stencilConfigPath)) { const { devServer, fsNamespace, outputTargets } = (await loadConfig({ configPath: stencilConfigPath })).config; - // Grab the WWW output target. If one doesn't exist, we'll throw a warning and roll - // with the default value for the entry path. + // Grab a suitable output target for script injection. + // Priority: www > dist (v4) > loader-bundle (v5) const wwwTarget = outputTargets.find((o): o is OutputTargetWww => o.type === 'www'); + const distTarget = outputTargets.find((o) => o.type === 'dist') as OutputTargetWithDir | undefined; + // loader-bundle is a v5 output target type, so we need to cast to avoid type errors on v4 + const loaderBundleTarget = outputTargets.find((o) => (o as OutputTargetWithDir).type === 'loader-bundle') as + | OutputTargetWithDir + | undefined; + if (wwwTarget) { // Get path from dev-server root to www - let relativePath = relative(devServer.root!, wwwTarget.dir!); - relativePath = relativePath === '' ? '.' : relativePath; - if (!relativePath.startsWith('.')) { - relativePath = `./${relativePath}`; - } + const relativePath = relative(devServer.root!, wwwTarget.dir!); + + // Use buildDir from config (defaults to 'build' for www target) + const buildDir = (wwwTarget as unknown as { buildDir?: string }).buildDir ?? 'build'; + const entryPath = join(relativePath, buildDir, fsNamespace); + stencilEntryPath = entryPath === '' ? '.' : entryPath.startsWith('.') ? entryPath : `./${entryPath}`; + } else if (distTarget || loaderBundleTarget) { + // Fall back to dist or loader-bundle target + const target = distTarget ?? loaderBundleTarget!; + + // Get path from dev-server root to target dir + const relativePath = relative(devServer.root!, target.dir!); - stencilEntryPath = `${relativePath}/build/${fsNamespace}`; + // dist/loader-bundle use empty string as default buildDir + // Path structure: dir/buildDir/namespace/namespace (extra namespace folder) + const buildDir = target.buildDir ?? ''; + const entryPath = join(relativePath, buildDir, fsNamespace, fsNamespace); + stencilEntryPath = entryPath === '' ? '.' : entryPath.startsWith('.') ? entryPath : `./${entryPath}`; } else { // Make a best guess at the entry path stencilEntryPath = `${DEFAULT_STENCIL_ENTRY_PATH_PREFIX}/${fsNamespace}`; console.warn( - `No "www" output target found in the Stencil config. Using default entry path: "${stencilEntryPath}". Tests using 'setContent' may fail to execute.`, + `No "www", "dist", or "loader-bundle" output target found in the Stencil config. Using default entry path: "${stencilEntryPath}". Tests using 'setContent' may fail to execute.`, ); } diff --git a/src/test/load-config-meta.spec.ts b/src/test/load-config-meta.spec.ts index 3a6f714..d0affcc 100644 --- a/src/test/load-config-meta.spec.ts +++ b/src/test/load-config-meta.spec.ts @@ -7,7 +7,17 @@ vi.mock('fs', () => ({ existsSync: () => existsSyncMock(), })); -const stencilConfig = { +const stencilConfig: { + fsNamespace: string; + devServer: { + protocol: string; + address: string; + port: number; + pingRoute: string; + root: string; + }; + outputTargets: Array<{ type: string; dir: string; buildDir?: string }>; +} = { fsNamespace: 'mock-namespace', devServer: { protocol: 'http', @@ -71,7 +81,7 @@ describe('loadConfigMeta', () => { }); }); - it('should log a warning if no "www" output target is found', async () => { + it('should log a warning if no supported output target is found', async () => { stencilConfig.outputTargets = []; existsSyncMock.mockReturnValueOnce(true); findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts'); @@ -80,7 +90,7 @@ describe('loadConfigMeta', () => { const configMeta = await loadConfigMeta(); expect(consoleWarnSpy).toHaveBeenCalledWith( - `No "www" output target found in the Stencil config. Using default entry path: "./build/mock-namespace". Tests using 'setContent' may fail to execute.`, + `No "www", "dist", or "loader-bundle" output target found in the Stencil config. Using default entry path: "./build/mock-namespace". Tests using 'setContent' may fail to execute.`, ); expect(configMeta).toEqual({ baseURL: 'http://localhost:4444', @@ -90,6 +100,119 @@ describe('loadConfigMeta', () => { }); }); + it('should use www target with custom buildDir', async () => { + stencilConfig.outputTargets = [ + { + type: 'www', + dir: '/mock-path/www', + buildDir: 'custom-build', + }, + ]; + existsSyncMock.mockReturnValueOnce(true); + findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts'); + + const configMeta = await loadConfigMeta(); + + expect(configMeta).toEqual({ + baseURL: 'http://localhost:4444', + stencilEntryPath: './www/custom-build/mock-namespace', + stencilNamespace: 'mock-namespace', + webServerUrl: 'http://localhost:4444/status', + }); + }); + + it('should fall back to dist target when www is not available', async () => { + stencilConfig.outputTargets = [ + { + type: 'dist', + dir: '/mock-path/dist', + }, + ]; + existsSyncMock.mockReturnValueOnce(true); + findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts'); + + const configMeta = await loadConfigMeta(); + + expect(configMeta).toEqual({ + baseURL: 'http://localhost:4444', + stencilEntryPath: './dist/mock-namespace/mock-namespace', + stencilNamespace: 'mock-namespace', + webServerUrl: 'http://localhost:4444/status', + }); + }); + + it('should fall back to loader-bundle target when www and dist are not available', async () => { + stencilConfig.outputTargets = [ + { + type: 'loader-bundle', + dir: '/mock-path/dist/loader-bundle', + }, + ]; + existsSyncMock.mockReturnValueOnce(true); + findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts'); + + const configMeta = await loadConfigMeta(); + + expect(configMeta).toEqual({ + baseURL: 'http://localhost:4444', + stencilEntryPath: './dist/loader-bundle/mock-namespace/mock-namespace', + stencilNamespace: 'mock-namespace', + webServerUrl: 'http://localhost:4444/status', + }); + }); + + it('should prefer www target over dist and loader-bundle', async () => { + stencilConfig.outputTargets = [ + { + type: 'dist', + dir: '/mock-path/dist', + }, + { + type: 'www', + dir: '/mock-path/www', + }, + { + type: 'loader-bundle', + dir: '/mock-path/dist/loader-bundle', + }, + ]; + existsSyncMock.mockReturnValueOnce(true); + findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts'); + + const configMeta = await loadConfigMeta(); + + expect(configMeta).toEqual({ + baseURL: 'http://localhost:4444', + stencilEntryPath: './www/build/mock-namespace', + stencilNamespace: 'mock-namespace', + webServerUrl: 'http://localhost:4444/status', + }); + }); + + it('should prefer dist target over loader-bundle', async () => { + stencilConfig.outputTargets = [ + { + type: 'loader-bundle', + dir: '/mock-path/dist/loader-bundle', + }, + { + type: 'dist', + dir: '/mock-path/dist', + }, + ]; + existsSyncMock.mockReturnValueOnce(true); + findUpMock.mockResolvedValueOnce('/mock-path/stencil.config.ts'); + + const configMeta = await loadConfigMeta(); + + expect(configMeta).toEqual({ + baseURL: 'http://localhost:4444', + stencilEntryPath: './dist/mock-namespace/mock-namespace', + stencilNamespace: 'mock-namespace', + webServerUrl: 'http://localhost:4444/status', + }); + }); + it('should log a warning if no Stencil config path was found', async () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});