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

Playwright integration #77

Open
pogodins opened this issue Jul 25, 2022 · 8 comments · Fixed by pactflow/docs.pactflow.io#163
Open

Playwright integration #77

pogodins opened this issue Jul 25, 2022 · 8 comments · Fixed by pactflow/docs.pactflow.io#163

Comments

@pogodins
Copy link

It's very helpful to use mocks for functional testing as a base for consumer contract.
Would be nice to support generation of Pact files from Playwright mocks similarly to how it's done for Cypress.

@mefellows
Copy link
Contributor

Thanks @pogodins, we'll add this for consideration.

One thing about Playwright is that mocks aren't created through a central API (unlike Cypress, MSW etc.) but left up to the user to implement how they wish. This might make it hard to create a general purpose adapter.

Do you have any suggestions as to how we might be able to navigate this?

Also if you are currently using Playwright, you could create your own contracts from them or even contribute an adapter for the community.

@pogodins
Copy link
Author

pogodins commented Jul 27, 2022

Thanks @mefellows for looking into this!

I think there's a difference between mocks (what you have found) and network calls interception like in Cypress. I wasn't even aware of the Mock APIs in Playwright.

I'm more interested in Network calls interception:

Route API can be used as a base for Pact contracts generation:

Please let me know if this give you enough information or you'll need more clarification.

@mefellows
Copy link
Contributor

mefellows commented Jul 27, 2022

Aha, yes that would definitely be a better option!

Thanks for raising. For transparency, our roadmap is close to being locked in for the next quarter and we are very unlikely to be able to squeeze this in, so this is something we won't be looking to pick up until at least Q4.

If you are interested in building it in the mean time, do reach out and we'll be happy to support.

cc: @YOU54F

@YOU54F
Copy link
Member

YOU54F commented Aug 24, 2022

Hey @pogodins

Thanks for the additional links. I've knocked a little something up that you can try and modify to suit your needs.

As Matt eluded to, we've tried to make it easy to teams to make their own adapters, with this guide, as you can see below there isn't much code to it.

This was based off the mountebank example https://github.com/pactflow/example-bi-directional-consumer-mountebank/blob/master/test/mountebankSerialiser.js#L27

import * as fs from 'fs'
import * as path from 'path'
import { omit } from 'lodash'

const AUTOGEN_HEADER_BLOCKLIST = [
  'access-control-expose-headers',
  'access-control-allow-credentials',
  'vary',
  'host',
  'proxy-connection',
  'sec-ch-ua',
  'sec-ch-ua-mobile',
  'user-agent',
  'sec-ch-ua-platform',
  'origin',
  'sec-fetch-site',
  'sec-fetch-mode',
  'sec-fetch-dest',
  'referer',
  'accept-encoding',
  'accept-language',
  'date',
  'x-powered-by'
]

const removeDuplicates = (pact) => {
  let descriptions = {}

  pact.interactions = pact.interactions.reduce((acc, interaction) => {
    if (!descriptions[interaction.description]) acc.push(interaction)
    descriptions[interaction.description] = true

    return acc
  }, [])

  return pact
}

const writePact = (pact, filePath, keepDupeDescs) => {
  createPactDir()
  const cleanPact = removeDuplicates(pact)

  fs.writeFileSync(filePath, JSON.stringify(keepDupeDescs ? cleanPact : pact))
}

const createPactDir = () => {
  try {
    fs.mkdirSync('./pacts')
  } catch (e) {
    // likely dir already exists
  }
}

const readPactFileOrDefault = (filePath, defaultPact) => {
  let pact = {}

  try {
    const res = fs.readFileSync(filePath)
    pact = JSON.parse(res.toString('utf8'))
  } catch (e) {
    pact = defaultPact
  }

  return pact
}

export const transformPlaywrightMatchToPact = async (route, opts) => {
  const { pacticipant, provider, keepDupeDescs } = opts
  const filePath = path.join('pacts', `${pacticipant}-${provider}.json`)

  const defaultPact = {
    consumer: { name: pacticipant },
    provider: { name: provider },
    interactions: [],
    metadata: {
      pactSpecification: {
        version: '2.0.0'
      },
      client: {
        name: 'pact-playwright-js-adapter',
        version: '0.0.1'
      }
    }
  }
  const pact = readPactFileOrDefault(filePath, defaultPact)
  const url = new URL(route.request().url())
  const request = route.request()
  const resp = await request.response()
  const respBody = await resp?.body()

  const matches = [
    {
      description: `pw_${request.method()}_${url.pathname}_${resp?.status()}${
        url.searchParams.toString() ? '_' + url.searchParams.toString() : ''
      }`,
      request: {
        method: route.request().method(),
        path: url.pathname,
        body: request.postDataJSON() ? request.postDataJSON() : undefined,
        query: url.searchParams.toString() ? url.searchParams.toString() : undefined,
        headers: request.headers() ? omit(request.headers(), [...AUTOGEN_HEADER_BLOCKLIST]) : {}
      },
      response: {
        status: resp?.status(),
        headers: resp?.headers() ? omit(resp?.headers(), [...AUTOGEN_HEADER_BLOCKLIST]) : {},
        body: respBody ? JSON.parse(respBody?.toString()) : undefined
      }
    }
  ]

  pact.interactions = [...pact.interactions, ...matches.flat()]

  writePact(pact, filePath, keepDupeDescs)
}

You can use it in your Playwright test like so

const { test, expect } = require('@playwright/test')
const testData = require('./fixtures/product.json')
const { transformPlaywrightMatchToPact } = require('./playwrightSerialiser')

test('product page', async ({ page }) => {
  const productApiPath = process.env.REACT_APP_API_BASE_URL
    ? process.env.REACT_APP_API_BASE_URL
    : 'http://localhost:8080'

  await page.route(productApiPath + '/product/*', async (route) => {
    route.fulfill({
      status: 200,
      body: JSON.stringify(testData),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    const pacticipant = 'pactflow-example-bi-directional-consumer-playwright'
    const provider = process.env.PACT_PROVIDER || 'pactflow-example-bi-directional-provider-dredd'
    await transformPlaywrightMatchToPact(route, { pacticipant, provider })
    return
  })
  await page.goto('http://localhost:3000/products/09')

  expect(await page.locator('.product-id').textContent()).toBe('ID: 09')
  expect(await page.locator('.product-name').textContent()).toBe('Name: Gem Visa')
  expect(await page.locator('.product-type').textContent()).toBe('Type: CREDIT_CARD')
})

@YOU54F
Copy link
Member

YOU54F commented Oct 27, 2022

Repo with above code in

https://github.com/pactflow/example-bi-directional-consumer-playwright

@mefellows
Copy link
Contributor

Nice, should we get that up here Yousaf https://docs.pactflow.io/docs/examples?

@pogodins
Copy link
Author

@YOU54F thanks for helping! One of our developers was able to generate Pact files with the help of your first example. Will check new repository created by you as well.

@YOU54F
Copy link
Member

YOU54F commented Oct 28, 2022

Will re-open, if I community contributor wants to create an npm package out the adapter, we would <3 it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants