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

add integration test in CI #151

Closed
wants to merge 11 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ defaults: &defaults
REACT_DIST_TAG: << parameters.react-dist-tag >>
working_directory: /tmp/material-ui-x
docker:
- image: circleci/node:10
- image: circleci/node:latest-browsers
Copy link
Member

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

Suggested change
- image: circleci/node:latest-browsers
- image: circleci/node:10

For the missing dependencies, we have prepare_chrome_headless the step which should work with the non headless version too

# CircleCI has disabled the cache across forks for security reasons.
# Following their official statement, it was a quick solution, they
# are working on providing this feature back with appropriate security measures.
@@ -103,6 +103,16 @@ jobs:
# hardcoded in karma-webpack
path: /tmp/_karma_webpack_
destination: artifact-file
test_puppeteer:
<<: *defaults
steps:
- checkout
- install_js
- run:
name: 'Running Storybook integration tests'
command: yarn test:e2e
- store_artifacts:
path: /tmp/material-ui-x/packages/storybook/integration/__image_snapshots__/__diff_output__
workflows:
version: 2
pipeline:
@@ -117,3 +127,8 @@ workflows:
- test_browser:
requires:
- checkout
- test_puppeteer:
requires:
- checkout


1 change: 1 addition & 0 deletions package.json
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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering storybook is a workspace?

Suggested change
"test:e2e": "cd packages/storybook && yarn test:ci",
"test:e2e": "yarn workspace storybook test:ci",

"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"
12 changes: 12 additions & 0 deletions packages/storybook/Dockerfile
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
21 changes: 21 additions & 0 deletions packages/storybook/LICENSE
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.
9 changes: 9 additions & 0 deletions packages/storybook/Makefile
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
13 changes: 13 additions & 0 deletions packages/storybook/docker-compose.yml
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"]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion packages/storybook/integration/components.test.ts
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
done();

});
afterAll(async () => {
await stopBrowser();
Copy link
Member

@oliviertassinari oliviertassinari Aug 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, why are startBrowser and stopBrowser needed? Isn't this feature provided by https://github.com/smooth-code/jest-puppeteer/blob/69a0c3ee80ef4b6af08d45ff44229df6ced8b666/packages/jest-puppeteer-preset/jest-preset.json#L2 in the first place?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

@oliviertassinari oliviertassinari Aug 8, 2020

Choose a reason for hiding this comment

The 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:

});
});
65 changes: 48 additions & 17 deletions packages/storybook/integration/helper-fn.ts
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() {
Copy link
Member

Choose a reason for hiding this comment

The 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 => {
5 changes: 3 additions & 2 deletions packages/storybook/integration/staticStories.test.ts
Original file line number Diff line number Diff line change
@@ -96,12 +96,13 @@ const stories = [
];

describe('snapshotTest', () => {
stories.forEach((config: any) => {
stories.forEach((config: any, index: number) => {
const path = typeof config === 'string' ? config : config.path;
const beforeTest = typeof config === 'string' ? undefined : config.beforeTest;
const isLatestStory = stories.length - 1 === index;

test(path, async (done) => {
await snapshotTest(path, beforeTest);
await snapshotTest(path, beforeTest, isLatestStory);
done();
});
});
1 change: 1 addition & 0 deletions packages/storybook/package.json
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@
"lint": "../../node_modules/.bin/tsc --noEmit && eslint 'src/**/*.{ts,tsx}' --quiet --fix -c ./.eslintrc.js && npm run lint:css",
"lint:css": "stylelint 'src/**/*.{ts,tsx}' ../../.stylelintrc.js",
"test": "jest -c integration/setup/jest.config.js --detectOpenHandles --force-exit --runInBand",
"test-update": "jest -c integration/setup/jest.config.js --detectOpenHandles --force-exit --runInBand --updateSnapshot",
"test:ci": "start-server-and-test start http://localhost:6006 test"
}
}