Convert CodeceptJS to Playwright

JavaScript pattern

Migrate from CodeceptJS to Playwright.

Converts Codecept property

BEFORE
// @filename: test.js
const { I } = inject();

export default {
  url: 'https://grit.io',
  selector: locate('#migration-selector').as('Selector'),
  openai: locate('text=custodian-sample-org/openai-quickstart-python').as('Openai'),
};
AFTER
// @filename: test.js
import { expect } from '@playwright/test';

export default class Test extends BasePage {
  get url() {
    return 'https://grit.io';
  }
  get selector() {
    return this.page.locator('#migration-selector');
  }
  get openai() {
    return this.page.locator('text=custodian-sample-org/openai-quickstart-python');
  }
}

Converts waiters

BEFORE
// @filename: someFolder/test.js
const { I } = inject();

export default {
  url: 'https://grit.io',

  waitForGrit() {
    I.waitInUrl(this.url);
    I.waitForText('Studio', 10, this.heading);
    I.see('Function expressions to arrow functions', this.rewrite);
    I.click(this.button);
    I.waitForVisible(this.rewritten);
  },
};
AFTER
// @filename: someFolder/test.js
import { expect } from '@playwright/test';

export default class Test extends BasePage {
  get url() {
    return 'https://grit.io';
  }

  async waitForGrit() {
    await this.page.waitForURL(new RegExp(this.url));
    await expect(this.heading).toHaveText('Studio', {
      timeout: 10 * 1000,
      ignoreCase: true,
    });
    await expect(this.rewrite).toContainText('Function expressions to arrow functions');
    await this.button.click();
    await this.rewritten.waitFor({ state: 'visible' });
  }
}

Converts complex locators

BEFORE
// @filename: someFolder/test.js
const { I } = inject();

export default {
  studio: locate('.studio'),
  message: 'Hello world',
  button: (name) => locate(`//button[contains(text(), "${name}")]`).as(name),

  waitForGrit() {
    I.waitForVisible(this.studio.withText(this.message), 5);
    I.click(this.button('grit').at(2), this.studio);
    I.seeCssPropertiesOnElements(this.studio, {
      'background-color': '#3570b6',
      display: 'flex',
    });
    I.seeAttributesOnElements(this.studio, {
      open: true,
      'grit-label': 'nice',
    });
    let lines = I.grabNumberOfVisibleElements(locate('div').withDescendant('p'));
    I.seeNumberOfVisibleElements(locate('div').withDescendant('p'), lines);
  },
};
AFTER
// @filename: someFolder/test.js
import { expect } from '@playwright/test';

export default class Test extends BasePage {
  get studio() {
    return this.page.locator('.studio');
  }
  get message() {
    return 'Hello world';
  }
  button(name) {
    return this.page.locator(`//button[contains(text(), "${name}")]`);
  }

  async waitForGrit() {
    await this.studio
      .and(this.page.locator(`:has-text("${this.message}")`))
      .waitFor({ state: 'visible', timeout: 5 * 1000 });
    await this.studio.locator(this.button('grit').nth(1)).click();
    await expect(this.studio).toHaveCSS('background-color', '#3570b6');
    await expect(this.studio).toHaveCSS('display', 'flex');
    await expect(this.studio).toHaveAttribute('open', 'true');
    await expect(this.studio).toHaveAttribute('grit-label', 'nice');
    let lines = await this.page
      .locator('div')
      .filter({ has: this.page.locator('p') })
      .count();
    expect(
      await this.page
        .locator('div')
        .filter({ has: this.page.locator('p') })
        .count(),
    ).toEqual(lines);
  }
}

Converts Codecept scenario

BEFORE
Scenario('Trivial test', async ({ I }) => {
  projectPage.open();
  I.waitForVisible(projectPage.list);
  I.refreshPage();
  I.see(projectPage.demo, projectPage.list);
  expect(true).toBe(true);
  projectPage.close();
})
  .tag('Email')
  .tag('Studio')
  .tag('Projects');
AFTER
import { expect } from '@playwright/test';

test('Trivial test @Email @Studio @Projects', async ({ page, factory, context }) => {
  var projectPage = new ProjectPage(page, context);
  await projectPage.open();
  await projectPage.list.waitFor({ state: 'visible' });
  await page.reload();
  await expect(projectPage.list).toContainText(projectPage.demo);
  await expect(true).toBe(true);
  await projectPage.close();
});

Does not convert inner object properties to getters

BEFORE
// @filename: someFolder/test.js
const { I } = inject();

export default {
  studio: locate('.studio'),
  message: 'Hello world',

  section: {
    editor: locate('#editor'),
    title: 'Apply a GritQL pattern',
  },
  someMethod() {
    return {
      foo: bar,
    };
  },
};
AFTER
// @filename: someFolder/test.js
import { expect } from '@playwright/test';

export default class Test extends BasePage {
  get studio() {
    return this.page.locator('.studio');
  }
  get message() {
    return 'Hello world';
  }

  get section() {
    return {
      editor: this.page.locator('#editor'),
      title: 'Apply a GritQL pattern',
    };
  }
  async someMethod() {
    return {
      foo: bar,
    };
  }
}

Converts Codecept scenario with multiple args and parentheses in description

BEFORE
Scenario('Trivial test (good)', async ({ I, loginAs }) => {
  projectPage.open();
  listModal.open();
  patternsList.open();
  I.waitForVisible(projectPage.list);
})
  .tag('Email')
  .tag('Studio')
  .tag('Projects')
  .tag('Multiword tag');
AFTER
import { expect } from '@playwright/test';

test('Trivial test (good) @Email @Studio @Projects @Multiword tag', async ({
  page,
  factory,
  context,
}) => {
  var projectPage = new ProjectPage(page, context);
  var listModal = new ListModal(page, context);
  var patternsList = new PatternsList(page, context);
  await projectPage.open();
  await listModal.open();
  await patternsList.open();
  await projectPage.list.waitFor({ state: 'visible' });
});

Converts parameterized tests

BEFORE
let myData = new DataTable(['id', 'name', 'capital']);
myData.add([1, 'England', 'London']);
myData.add([2, 'France', 'Paris']);
myData.add([3, 'Germany', 'Berlin']);
myData.add([4, 'Italy', 'Rome']);

Data(myData)
  .Scenario('Trivial test', { myData }, async ({ I, current }) => {
    I.say(current.capital);
    I.dragAndDrop(data.label, data.map);
    I.dragToPoint(data.label, 400, 0);
  })
  .tag('Email')
  .tag('Studio')
  .tag('Projects');
AFTER
import { expect } from '@playwright/test';

let myData = [
  { id: 1, name: 'England', capital: 'London' },
  { id: 2, name: 'France', capital: 'Paris' },
  { id: 3, name: 'Germany', capital: 'Berlin' },
  { id: 4, name: 'Italy', capital: 'Rome' },
];

for (const current of myData) {
  test('Trivial test @Email @Studio @Projects', async ({ page, factory, context }) => {
    console.log(current.capital);
    await data.label.dragTo(data.map);
    await data.label.dragTo(data.label, { targetPosition: { x: 400, y: 0 } });
  });
}

Wraps tests in describe block

BEFORE
Feature('Test capitals');

import { Capitals } from '../data/capitals';

let myData = new DataTable(['id', 'name', 'capital']);
myData.add([1, 'England', Capitals.London]);
myData.add([2, 'France', Capitals.Paris]);
myData.add([3, 'Germany', Capitals.Berlin]);
myData.add([4, 'Italy', Capitals.Rome]);

Data(myData)
  .Scenario('Trivial test', { myData }, async ({ I, current }) => {
    I.say(current.capital);
  })
  .tag('Email')
  .tag('Studio')
  .tag('Projects');
AFTER
import { Capitals } from '../data/capitals';
import { expect } from '@playwright/test';

test.describe('Test capitals', () => {
  let myData = [
    { id: 1, name: 'England', capital: Capitals.London },
    { id: 2, name: 'France', capital: Capitals.Paris },
    { id: 3, name: 'Germany', capital: Capitals.Berlin },
    { id: 4, name: 'Italy', capital: Capitals.Rome },
  ];

  for (const current of myData) {
    test('Trivial test @Email @Studio @Projects', async ({ page, factory, context }) => {
      console.log(current.capital);
    });
  }
});

Converts tests with backtick descriptions

BEFORE
let myData = new DataTable(['id', 'name', 'capital']);
myData.add([1, 'England', 'London']);
myData.add([2, 'France', 'Paris']);
myData.add([3, 'Germany', 'Berlin']);
myData.add([4, 'Italy', 'Rome']);

Data(myData)
  .Scenario(`Trivial test`, { myData }, async ({ I, current }) => {
    I.say(current.capital);
  })
  .tag('Email')
  .tag('Studio')
  .tag('Projects');
AFTER
import { expect } from '@playwright/test';

let myData = [
  { id: 1, name: 'England', capital: 'London' },
  { id: 2, name: 'France', capital: 'Paris' },
  { id: 3, name: 'Germany', capital: 'Berlin' },
  { id: 4, name: 'Italy', capital: 'Rome' },
];

for (const current of myData) {
  test(`Trivial test @Email @Studio @Projects`, async ({ page, factory, context }) => {
    console.log(current.capital);
  });
}

Intelligently converts stringlike locators

BEFORE
Scenario('Trivial test', async ({ I }) => {
  project_page.open();
  I.waitForVisible('.list' + ' ' + className);
  I.waitNumberOfVisibleElements('.grit-sample', 3);
  I.seeInField(`input[name="${username}"]`, 'admin');
})
  .tag('Email')
  .tag('Studio')
  .tag('Projects');
AFTER
import { expect } from '@playwright/test';

test('Trivial test @Email @Studio @Projects', async ({ page, factory, context }) => {
  var project_page = new ProjectPage(page, context);
  await project_page.open();
  await page.locator('.list' + ' ' + className).waitFor({ state: 'visible' });
  await expect(page.locator('.grit-sample')).toHaveCount(3);
  await expect(page.locator(`input[name="${username}"]`)).toHaveValue('admin');
});

Converts Before and After hooks

BEFORE
Feature('Project page');

BeforeSuite(({ I }) => {
  I.say('Ensure that you have access to the project');
});

Scenario('Trivial test', async ({ I }) => {
  projectPage.open();
})
  .tag('Email')
  .tag('Studio')
  .tag('Projects');

After(async ({ I }) => {
  await resetProjectSettings();
});
AFTER
import { expect } from '@playwright/test';

test.describe('Project page', () => {
  test.beforeAll(async ({ page, request }) => {
    console.log('Ensure that you have access to the project');
  });

  test('Trivial test @Email @Studio @Projects', async ({ page, factory, context }) => {
    var projectPage = new ProjectPage(page, context);
    await projectPage.open();
  });

  test.afterEach(async ({ page, request }) => {
    await resetProjectSettings();
  });
});