Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(percy): support builder for options and adding types #690

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
94 changes: 82 additions & 12 deletions packages/histoire-plugin-percy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,79 @@ import path from 'pathe'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import { isPercyEnabled, fetchPercyDOM, postSnapshot } from '@percy/sdk-utils'
import type { JSONObject, Page, WaitForOptions } from 'puppeteer'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)

/**
* Percy Snapshot Options
* Not official type, just for reference
* @see https://www.browserstack.com/docs/percy/take-percy-snapshots/snapshots-via-scripts
*/
export interface PercySnapshotOptions {
widths?: number[]
minHeight?: number
percyCSS?: string
enableJavaScript?: boolean
discovery?: Partial<{
allowedHostnames: string[]
disallowedHostnames: string[]
requestHeaders: Record<string, string>
authorization: Partial<{
username: string
password: string
}>
disableCache: boolean
userAgent: string
}>
}

export type PagePayload = {
file: string
story: { title: string }
variant: { id: string, title: string }
};

type ContructorOption<T extends object | number> =
| T
| ((payload: PagePayload) => T);

export interface PercyPluginOptions {
/**
* Ignored stories.
*/
ignored?: (payload: { file: string, story: { title: string }, variant: { id: string, title: string } }) => boolean
ignored?: (payload: PagePayload) => boolean
/**
* Percy options.
*/
percyOptions?: any
percyOptions?: ContructorOption<PercySnapshotOptions>

/**
* Delay puppeteer page screenshot after page load
*/
pptrWait?: number
pptrWait?: ContructorOption<number>

/**
* Navigation Parameter
*/
pptrOptions?: any
pptrOptions?: ContructorOption<
WaitForOptions & {
referer?: string
}
>

/**
* Before taking a snapshot, you can modify the page
* It happens after the page is loaded and wait (if pptrWait is passed) and before the snapshot is taken
*
* @param page Puppeteer page
* @returns Promise<void | boolean> - If it returns false, the snapshot will be skipped
*/
beforeSnapshot?: (
page: Page,
payload: PagePayload
) => Promise<void | boolean>
}

const defaultOptions: PercyPluginOptions = {
Expand All @@ -35,13 +85,20 @@ const defaultOptions: PercyPluginOptions = {
pptrOptions: {},
}

function resolveOptions<T extends object | number> (
option: ContructorOption<T>,
payload: PagePayload,
): T {
return typeof option === 'function' ? option(payload) : option
}

export function HstPercy (options: PercyPluginOptions = {}): Plugin {
const finalOptions: PercyPluginOptions = defu(options, defaultOptions)
return {
name: '@histoire/plugin-percy',

onBuild: async api => {
if (!await isPercyEnabled()) {
onBuild: async (api) => {
if (!(await isPercyEnabled())) {
return
}

Expand All @@ -55,7 +112,7 @@ export function HstPercy (options: PercyPluginOptions = {}): Plugin {
const ENV_INFO = `${puppeteerPkg.name}/${puppeteerPkg.version}`

api.onPreviewStory(async ({ file, story, variant, url }) => {
if (finalOptions.ignored?.({
const payload = {
file,
story: {
title: story.title,
Expand All @@ -64,23 +121,36 @@ export function HstPercy (options: PercyPluginOptions = {}): Plugin {
id: variant.id,
title: variant.title,
},
})) {
}

if (finalOptions.ignored?.(payload)) {
return
}

const pptrOptions = resolveOptions(finalOptions.pptrOptions, payload)
const pptrWait = resolveOptions(finalOptions.pptrWait, payload)
const percyOptions = resolveOptions(finalOptions.percyOptions, payload)

const page = await browser.newPage()
await page.goto(url, finalOptions.pptrOptions)
await page.goto(url, pptrOptions)

await new Promise((resolve) => setTimeout(resolve, pptrWait))

await new Promise(resolve => setTimeout(resolve, finalOptions.pptrWait))
if (finalOptions.beforeSnapshot) {
const result = await finalOptions.beforeSnapshot(page, payload)
if (result === false) {
return
}
}

const name = `${story.title} > ${variant.title}`
await page.evaluate(await fetchPercyDOM())
const domSnapshot = await page.evaluate((opts) => {
// @ts-expect-error window global var
return window.PercyDOM.serialize(opts)
}, finalOptions.percyOptions)
}, percyOptions as JSONObject)
await postSnapshot({
...finalOptions.percyOptions,
...percyOptions,
environmentInfo: ENV_INFO,
clientInfo: CLIENT_INFO,
url: page.url(),
Expand Down