shakacode / cypress-on-rails

Use cypress.io or playwright.dev with your rails application. This Ruby gem lets you use your regular Rails test setup and clean-up, such as FactoryBot.
MIT License
416 stars 61 forks source link

Consider Playwright support (or playwright-on-rails) #116

Closed d4rky-pl closed 1 year ago

d4rky-pl commented 2 years ago

I was able to successfully reuse your library to work with Playwright. The only thing required was porting Cypress commands to Playwright. You should consider either forking this library and changing the naming or creating a common core and make both libraries swappable :)

justin808 commented 2 years ago

@Romex91 @lukgni, we somehow need to make this happen!

tvongaza commented 1 year ago

For anyone curious, I've followed @d4rky-pl approach and gotten playwright working. The setup is really only using thin slices of the middleware from Cypress on Rails, seems like these could get abstracted out to be used with any E2E testing framework. Would definitely simplify some of the assumptions.

I've got everything setup under spec/playwright, and playwright wired up to accordingly. So far playwright has been very consistent and very fast. We sometimes hit snags where a spec runs fine locally but not on CI, but they aren't flakey and always error out at the same location so usually takes some refinement to get right.

I've also got a script which aids with using the playwright codegen tool against the same reproducible rails test environment. Happy to share if anyone is interested in it.

The key file is in spec/playwright/support/onrails.ts pulled from @d4rky-pl's tweet which looks like:

import { request, expect } from '@playwright/test'
import { config } from '../../../playwright.config'

const contextPromise = request.newContext({ baseURL: config.use?.baseURL })

const appCommands = async (data: { name: any; options: {} }) => {
  const context = await contextPromise
  const response = await context.post('/__cypress__/command', { data })

  expect(response.ok()).toBeTruthy()
  return response.body
}

const app = (name: string, options = {}) => appCommands({ name, options }).then((body) => body[0])
const appScenario = (name: string, options = {}) => app('scenarios/' + name, options)
const appEval = (code: {} | undefined) => app('eval', code)
const appFactories = (options: {} | undefined) => app('factory_bot', options)

const appVcrInsertCassette = async (cassette_name: any, options: { [x: string]: any; }) => {
  const context = await contextPromise;
  if (!options) options = {};

  Object.keys(options).forEach(key => options[key] === undefined ? delete options[key] : {});
  const response = await context.post("/__cypress__/vcr/insert", {data: [cassette_name,options]});
  expect(response.ok()).toBeTruthy();
  return response.body;
}

const appVcrEjectCassette = async () => {
  const context = await contextPromise;

  const response = await context.post("/__cypress__/vcr/eject");
  expect(response.ok()).toBeTruthy();
  return response.body;
}

export { appCommands, app, appScenario, appEval, appFactories, appVcrInsertCassette, appVcrEjectCassette }

Note if using VCR cassettes, you'll want to ensure you're server is started in single worker/threaded mode, I've done this a playwright specific procfile Procefile.playwright.test:

vite: bin/vite dev -m test --clobber
# For VCR to work we need to start puma in single worker mode (-w 0) and with exactly one thread (-t 1:1)
# We also need to run our Good Job's inline such that they use the VCR
web: GOOD_JOB_EXECUTION_MODE=inline bundle exec puma -t 1:1 -w 0 -p 5017  -e test

This is used by a bin script which just runs foreman with that procefile, then in my playwright.config.ts I've got the following to start up the app. Not the /tests/healthcheck url, implement that how you will:

  webServer: {
    command: 'bin/playwright-test',
    url: `${baseURL}/tests/healthcheck`,
    reuseExistingServer: !process.env.CI,
  },

Tests can make use the helper methods from onrails.ts, ie:

import { test, expect } from '@playwright/test';
import { app, appScenario } from './onrails';

test('has title', async ({ page }) => {
  await app('clean');
  await appScenario('basic');

  await page.goto('/');

  // Expect a title "to contain" a substring.
  await expect(page).toHaveTitle(/Some Title/);
});

Or in your global setup/tear down scripts:

// global-setup.ts/js
import { chromium, FullConfig } from '@playwright/test';
import { app, appScenario } from './onrails';

async function globalSetup(config: FullConfig) {
  await app('clean');
  await appScenario('basic');

  const { baseURL, storageState } = config.projects[0].use;
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(`${baseURL}/tests/login`);
  await page.context().storageState({ path: storageState as string });
  await browser.close();
}

export default globalSetup;
justin808 commented 1 year ago

@tvongaza thank you so much! BTW, let me know if you want to submit any PR for this. Otherwise, I hope somebody else does this soon.