-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
add integration test in CI #151
Changes from all commits
7e6a843
9ac485f
23dbafb
7a283a7
d913ed7
82f148c
a6bfaf0
8a0296a
e52f8b7
deff3e7
22d8b0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -81,6 +81,7 @@ | |||||
"test:karma": "cross-env NODE_ENV=test karma start test/karma.conf.js", | ||||||
"test:unit": "cross-env NODE_ENV=test mocha 'packages/**/*.test.tsx' --exclude '**/node_modules/**'", | ||||||
"test:watch": "yarn test:unit --watch", | ||||||
"test:e2e": "cd packages/storybook && yarn test:ci", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Considering storybook is a workspace?
Suggested change
|
||||||
"lint": "yarn eslint && yarn jsonlint", | ||||||
"eslint": "eslint . --cache --report-unused-disable-directives --ext .js,.ts,.tsx", | ||||||
"eslint:ci": "eslint . --report-unused-disable-directives --ext .js,.ts,.tsx" | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
FROM node:8 | ||
|
||
RUN apt-get update | ||
|
||
# for https | ||
RUN apt-get install -yyq ca-certificates | ||
# install libraries | ||
RUN apt-get install -yyq libappindicator1 libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 | ||
# tools | ||
RUN apt-get install -yyq gconf-service lsb-release wget xdg-utils | ||
# and fonts | ||
RUN apt-get install -yyq fonts-liberation |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2017 Vladislav Supalov | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
rebuild: | ||
mkdir -p output | ||
docker-compose down | ||
docker-compose build | ||
docker-compose up | ||
enter: | ||
docker-compose exec puppeteer bash | ||
|
||
.PHONY: rebuild enter |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
version: '3' | ||
|
||
services: | ||
puppeteer: | ||
build: . | ||
volumes: | ||
# mount code and output directories into the container | ||
- ./output:/output | ||
- .:/app | ||
working_dir: /app | ||
shm_size: 1gb #512M | ||
# just run the container doing nothing | ||
entrypoint: ["sh", "-c", "sleep infinity"] |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,4 +1,4 @@ | ||||
import { snapshotTest } from './helper-fn'; | ||||
import { snapshotTest, stopBrowser } from './helper-fn'; | ||||
|
||||
jest.setTimeout(30000); | ||||
|
||||
|
@@ -31,4 +31,7 @@ describe('Components override', () => { | |||
await snapshotTest('/story/x-grid-demos-custom-components--styled-columns'); | ||||
done(); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
}); | ||||
afterAll(async () => { | ||||
await stopBrowser(); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused, why are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No as it's puppeter the web driver so you need to load an instance of the browser from its package. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't get it. From what I understand, we use jest-puppeteer, which is meant to handle the launch and close of the browser:
|
||||
}); | ||||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,10 @@ import { MatchImageSnapshotOptions } from 'jest-image-snapshot'; | |
const puppeteer = require('puppeteer'); | ||
|
||
export const NO_ANIM_CSS = ` | ||
* { | ||
text-rendering: geometricprecision !important; | ||
font-family: Monospace !important; | ||
} | ||
:not(iframe) *, | ||
:not(iframe) *::before, | ||
:not(iframe) *::after { | ||
|
@@ -20,15 +24,33 @@ export const IMAGE_SNAPSHOT_CONFIG = { | |
}, | ||
} as MatchImageSnapshotOptions; | ||
|
||
let browserInstance; | ||
export async function startBrowser() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had a look at the helpers I wrote in the past to write tests with puppeteer, if that help: Usage, createContext() import config from 'config'
import { ACCOUNT_STATUSES, SCOPES } from 'api/constants'
import { createContext } from 'test/e2e/setup'
import signIn from 'test/e2e/modules/signIn'
import matchers from 'test/e2e/modules/matchers'
import screenshot from 'test/e2e/modules/screenshot'
describe('signIn', () => {
const context = createContext()
it('should work', async () => {
const account = await context.factory.create(
'Account',
{ status: ACCOUNT_STATUSES.APPROVED },
{ scopes: [SCOPES.CONTRIBUTOR] }
)
const { page, i18n } = context
await page.goto(`${config.get('contributor.url')}/sign-in`)
await matchers.toMatch(page, i18n.t('contributorSignIn:apply'), {
timeout: 'navigation',
})
await screenshot(page, 'contributor-sign-in')
await page.type('input[name="email"]', account.email)
await page.type('input[name="password"]', account.password)
await page.type('input[name="password"]', '\u000d')
await matchers.toMatchUrl(page, `${config.get('contributor.url')}/`)
await matchers.toMatch(page, i18n.t('contributorDashboard:imageRequiresEditing'), {
timeout: 'navigation',
})
await page.waitFor('[data-e2e="appBar.user.menu"]')
await screenshot(page, 'contributor-dashboard')
await page.click('[data-e2e="appBar.user.menu"]')
await matchers.toMatchElement(page, '[role="menu"] > div:first-child', {
text: `${account.firstName} ${account.lastName}`,
})
})
it('should redirect when trying to access the sign in page', async () => {
const account = await context.factory.create(
'Account',
{ status: ACCOUNT_STATUSES.APPROVED },
{ scopes: [SCOPES.CONTRIBUTOR] }
)
await signIn({ context, account, scope: SCOPES.CONTRIBUTOR })
const { page } = context
await page.goto(`${config.get('contributor.url')}/settings/profile`)
expect(page.url()).toBe(`${config.get('contributor.url')}/settings/profile`)
await page.goto(`${config.get('contributor.url')}/sign-in`)
expect(page.url()).toBe(`${config.get('contributor.url')}/`)
})
}) test/e2e/setup.js /* eslint-disable no-underscore-dangle, import/prefer-default-export, import/no-extraneous-dependencies */
/* global jasmine */
import fetch from 'isomorphic-fetch'
import knexfile from 'knexfile'
import puppeteer from 'puppeteer'
import log from 'modules/scripts/log'
import waitUntil from 'modules/async/waitUntil'
import hackTranslate from 'modules/i18n/hackTranslate'
import i18nInit from 'web/modules/i18n/server'
import getFactory from 'test/modules/api/factory'
import createKnex from 'api/services/database/createKnex'
import truncateAll from 'api/services/database/truncateAll'
import { IMAGE_STATUSES } from 'api/constants'
jasmine.DEFAULT_TIMEOUT_INTERVAL = 6000e3
async function getBrowserWSEndpoint() {
if (process.env.__BROWSER_WS_ENDPOINT__) {
return process.env.__BROWSER_WS_ENDPOINT__
}
const browserRemoteUrl = process.env.BROWSER_REMOTE_URL
if (!browserRemoteUrl) {
throw new Error('process.env.BROWSER_REMOTE_URL is missing')
}
const browserWSEndpoint = await waitUntil(async () => {
log.info({
name: 'browser',
msg: `Fetch data from ${browserRemoteUrl}`,
force: true,
})
let version
try {
version = await fetch(`${browserRemoteUrl}/json/version`, {
method: 'GET',
}).then(result => result.json())
} catch (err) {
// ...
}
return {
predicate: version,
result: version ? version.webSocketDebuggerUrl : null,
}
})
log.info({
name: 'browser',
msg: `Resolved browser endpoint ${browserWSEndpoint} `,
force: true,
})
process.env.__BROWSER_WS_ENDPOINT__ = browserWSEndpoint
return browserWSEndpoint
}
async function launchBrowser() {
const browserWSEndpoint = await getBrowserWSEndpoint()
return puppeteer.connect({ browserWSEndpoint, slowMo: 0 })
}
function handleError(err) {
throw err
}
const knexConfig = knexfile[process.env.NODE_ENV]
export function createContext() {
const context = {}
const screenSize = process.env.SCREEN_SIZE
if (!screenSize) {
throw new Error('Missing process.env.SCREEN_SIZE')
}
const [width, height] = screenSize.split('x').map(Number)
beforeAll(async () => {
// console.time('db')
context.knex = createKnex({ connection: { database: knexConfig.connection.database } })
context.factory = getFactory(context.knex.models)
// console.timeEnd('db')
await Promise.all([
truncateAll(context.knex),
(async () => {
// console.time('browser')
context.browser = await launchBrowser()
const pages = await context.browser.pages()
if (!pages.length) {
pages.push(await context.browser.newPage())
}
context.page = pages[0]
context.page.setDefaultNavigationTimeout(
process.env.NODE_ENV === 'production' ? 10e3 : 60e3
)
context.page.addListener('pageerror', handleError)
await context.page.setViewport({ width, height: height - 83 })
// console.timeEnd('browser')
})(),
(async () => {
// console.time('i18n')
const i18n = await i18nInit()
context.i18n = hackTranslate(i18n)
// console.timeEnd('i18n')
})(),
])
})
// Avoid duplicating beforeAll truncate
let truncateDo = false
beforeEach(async () => {
// Kill all the in-flight requests
const html =
'<!DOCTYPE html><html><head></head><body>beforeEach, kill all the pending requests</body></html>'
await context.page.goto(`data:text/html,${html}`)
await Promise.all([
(async () => {
if (truncateDo) {
await truncateAll(context.knex)
}
truncateDo = true
})(),
context.page._client.send('Network.clearBrowserCookies'),
])
await context.factory.create('Image', {
customerHomeBackground: true,
status: IMAGE_STATUSES.APPROVED_ONLINE,
})
})
afterAll(async () => {
context.page.removeListener('pageerror', handleError)
await Promise.all([context.browser.disconnect(), context.knex.destroy()])
})
return context
} |
||
const browser = await puppeteer.launch({ | ||
args: ['--disable-lcd-text'], | ||
defaultViewport: { width: 1600, height: 900 }, | ||
// headless: false | ||
}); | ||
return browser; | ||
if (!browserInstance) { | ||
// eslint-disable-next-line no-console | ||
console.log('Launching new browser'); | ||
browserInstance = await puppeteer.launch({ | ||
args: [ | ||
'--disable-lcd-text', | ||
'--no-sandbox', | ||
'--disable-setuid-sandbox', | ||
'--enable-font-antialiasing', | ||
'--font-render-hinting=medium', | ||
'--disable-gpu', | ||
], | ||
defaultViewport: { width: 1600, height: 900 }, | ||
}); | ||
} | ||
return browserInstance; | ||
} | ||
export async function stopBrowser() { | ||
if (browserInstance != null) { | ||
// eslint-disable-next-line no-console | ||
console.log('Stopping browser'); | ||
await browserInstance.close(); | ||
browserInstance = null; | ||
} | ||
} | ||
|
||
export async function getStoryPage( | ||
browser: any, | ||
path: string, | ||
|
@@ -49,19 +71,28 @@ export async function getStoryPage( | |
export async function snapshotTest( | ||
path: string, | ||
beforeTest?: (page) => Promise<void>, | ||
isLatestStory?: boolean, | ||
): Promise<void> { | ||
const browser = await startBrowser(); | ||
const page = await getStoryPage(browser, path); | ||
await page.addStyleTag({ content: NO_ANIM_CSS }); | ||
try { | ||
const browser = await startBrowser(); | ||
const page = await getStoryPage(browser, path); | ||
await page.addStyleTag({ content: NO_ANIM_CSS }); | ||
|
||
if (beforeTest) { | ||
await beforeTest(page); | ||
} | ||
if (beforeTest) { | ||
await beforeTest(page); | ||
} | ||
|
||
const image = await page.screenshot(); | ||
expect(image).toMatchImageSnapshot(IMAGE_SNAPSHOT_CONFIG); | ||
await page.close(); | ||
await browser.close(); | ||
const image = await page.screenshot(); | ||
expect(image).toMatchImageSnapshot(IMAGE_SNAPSHOT_CONFIG); | ||
await page.close(); | ||
} catch (err) { | ||
await stopBrowser(); | ||
throw err; | ||
} finally { | ||
if (isLatestStory) { | ||
await stopBrowser(); | ||
} | ||
} | ||
} | ||
|
||
export const activeCell = (rowIndex: number, colIndex: number): boolean => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prefer to run the tests with v10, as the closest version to the minimum version of node we support for the components. Right now, we support node v8, but plan to upgrade mui/material-ui#19301 (comment).
Also, in theory, we don't need to load this distribution, puppeteer comes with a binary of chrome in
node_modules/puppeteer/.local-chromium
For the missing dependencies, we have
prepare_chrome_headless
the step which should work with the non headless version too