I have created a small React app and I want to test it using Playwright component testing
I have 3 components: App -> ChildComponent -> ChildChildComponent
I want to render (mount) the ChildComponent directly, and make assertions on it, but when I do that, some ContextApi functions that are defined in the App in the normal flow, are now undefined as the App component is not part of the component test.
So i'v trying to render the ChildComponent together with a face ContextApi Provider and pass mocks of those undefined functions, and then I get an infinite render loop for some reason.
How can I go about this, as this use case is typical in react component test.
Here is the test with all my failed mocking attempts separated:
test.only("validate CharacterModal", async ({ page, mount }) => {
const data = ['some-mocked-irrelevant-data']
// const setCurrentCharacter = () => {};
// const setIsCharacterModalOpen = () => {};
// const setCurrentCharacterMocked = sinon.stub("setCurrentCharacter").callsFake(() => {});
// const setIsCharacterModalOpenMocked = sinon.stub("setCurrentCharacter").callsFake(() => {});
// const setCurrentCharacter = jest.fn();
// const setIsCharacterModalOpen = jest.fn();
// const setCurrentCharacter = (): void => {};
// const setIsCharacterModalOpen = (): void => {};
// const setIsCharacterModalOpen = (isCharacterModalOpen: boolean): void => {};
const AppContext = React.createContext<any>(null);
await page.route("**/users*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(data),
});
});
const component = await mount(
<AppContext.Provider value={{ setCurrentCharacterMocked, setIsCharacterModalOpenMocked }}>
<CharacterModal />
</AppContext.Provider>
);
expect(await component.getByRole("img").count()).toEqual(4);
});
The beforeMount hook can be used for this. I recently added docs about this: https://github.com/microsoft/playwright/pull/20593/files.
// playwright/index.jsx
import { beforeMount, afterMount } from '#playwright/experimental-ct-react/hooks';
// NOTE: It's probably better to use a real context
const AppContext = React.createContext(null);
beforeMount(async ({ App, hooksConfig }) => {
if (hooksConfig?.overrides) {
return (
<AppContext.Provider value={hooksConfig.overrides}>
<App />
</AppContext.Provider>
);
}
});
// src/CharacterModal.test.jsx
import { test, expect } from '#playwright/experimental-ct-react';
import { CharacterModal } from './CharacterModal';
test('configure context through hooks config', async ({ page, mount }) => {
const component = await mount(<CharacterModal />, {
hooksConfig: { overrides: 'this is given to the context' },
});
});
Related
In order to use MDX v2 with remark and rehype plugins in react applications created with Create React App v5 I have to use CRACO (v7) and especially its ability to handle a configuration that is returned as a promise/async function. The reason is that CRACO uses CJS module-based configuration, while the remark and rehype plugins are ES modules. Now I want to use MDXv2 with remark/rehype plugins in React Styleguidist too.
This is my craco.config.js:
const { addAfterLoader, loaderByName } = require('#craco/craco')
const mdxplagues = require('./mdxplugins.js')
// https://github.com/facebook/create-react-app/pull/11886#issuecomment-1055054685
const ForkTsCheckerWebpackPlugin =
process.env.TSC_COMPILE_ON_ERROR === 'true'
? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
: require('react-dev-utils/ForkTsCheckerWebpackPlugin');
module.exports = async (env) => {
const mdxplagueConfig = await mdxplagues()
return {
webpack: {
configure: (webpackConfig) => {
addAfterLoader(webpackConfig, loaderByName('babel-loader'), {
test: /\.(md|mdx)$/,
loader: require.resolve('#mdx-js/loader'),
/** #type {import('#mdx-js/loader').Options} */
options: mdxplagueConfig,
})
// https://github.com/facebook/create-react-app/pull/11886#issuecomment-1055054685
webpackConfig.plugins.forEach((plugin) => {
if (plugin instanceof ForkTsCheckerWebpackPlugin) {
plugin.options.issue.exclude.push({ file: '**/src/**/?(*.){spec,test,cy}.*' });
}
})
return webpackConfig
}
}
}
}
And this is my mdxplugins.js that returns an async function returing the needed plugin configurations:
const textrTypoEnDashes = (input) => {
return input
.replace(/ -- /gim, '–')
}
module.exports = async () => {
const remarkGfm = (await import('remark-gfm')).default
const remarkImages = (await import('remark-images')).default
const remarkTextr = (await import('remark-textr')).default
const remarkAccessibleEmoji = (await import('#fec/remark-a11y-emoji')).default
const rehypeSlug = (await import('rehype-slug')).default
const textrTypoApos = (await import('typographic-apostrophes')).default
const textrTypoQuotes = (await import('typographic-quotes')).default
const textrTypoPossPluralsApos = (await import('typographic-apostrophes-for-possessive-plurals')).default
const textrTypoEllipses = (await import('typographic-ellipses')).default
//const textrTypoEmDashes = (await import('typographic-em-dashes')).default
const textrTypoNumberEnDashes = (await import('typographic-en-dashes')).default
return {
remarkPlugins: [
remarkGfm,
remarkImages,
remarkAccessibleEmoji,
[remarkTextr, {
plugins: [
textrTypoApos,
textrTypoQuotes,
textrTypoPossPluralsApos,
textrTypoEllipses,
// textrTypoEmDashes,
textrTypoNumberEnDashes,
textrTypoEnDashes,
],
options: {
locale: 'en-us'
}
}],
],
rehypePlugins: [
rehypeSlug,
],
}
}
My problem is now that I need to apply this webpack/MDXv2 plugin configuration to styleguide.config.js (v13.0.0). I tried...
module.exports = async () => {
return {
...
}
}
...but this doesn't seem to be supported. A look into React Styleguidist's sources, namely scripts/config.ts seems to confirm this.
How can I synchronously resolve my MDX plugin configuration that I return via an async function from my mdxplugin.js CJS module within Styleguidist's CJS configuration module?
I want to compose fixtures. The first fixture should always be available (think of it as base class). second fixture will vary in different test files (think of it as derived class)
I tried following code and it's working as I was expecting. Is this ok to follow this approach or any better option available?
//baseFixture.js
import { test as base} from '#playwright/test';
interface MyFixtures {
fixture1: string;
}
export const test = base.extend<MyFixtures>({
fixture1: "fixture-one"
}, );
//derivedFixture.js
import {test as test1} from 'baseFixture'
interface MyFixtures2 {
fixture2: string;
}
export const test = test1.extend<MyFixtures2>({
fixture2: "fixture-two"
}, );
//in test_file.js
import {test} from 'derivedFixture'
test('should allow me use composed fixture', async ({ page, fixture1, fixture2 }) => {
console.log(`from first fixture ${fixture1}`)
console.log(`from second fixture ${fixture2}`)
});
Seems to me that you are using fixtures like POMs and you are overengineering tests. If it works for you and depending on what you want, then use it. If my assumption is correct instead of passing fixtures to another fixture pass POMs and you can even perform steps here to get that page into certain state and here is example from playwright page:
// my-test.js
const base = require('#playwright/test');
const { TodoPage } = require('./todo-page');
const { SettingsPage } = require('./settings-page');
// Extend base test by providing "todoPage" and "settingsPage".
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
exports.test = base.test.extend({
todoPage: async ({ page }, use) => {
// Set up the fixture.
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// Use the fixture value in the test.
await use(todoPage);
// Clean up the fixture.
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
exports.expect = base.expect;
Then in your test simply pass {todoPage} or {settingsPage} or both:
const { test, expect } = require('./my-test');
test.beforeEach(async ({ settingsPage }) => {
await settingsPage.switchToDarkMode();
});
test('basic test', async ({ todoPage, page }) => {
await todoPage.addToDo('something nice');
await expect(page.locator('.todo-item')).toContainText(['something nice']);
});
Also you can chain your fixtures here and reuse them, for eg. you could pass todoPage to settingsPage fixture:
settingsPage: async ({ todoPage}, use) => {
await use(new SettingsPage(page));
},
This way everything in todoPage will be executed then settingsPage and this is what you pass to your test, and I guess this is what you were trying to achive.
My approach is to use the base fixture as a dependent fixture in a derivative fixture:
import { test as base } from "#playwright/test"
interface MyFixtures1 {
fixture1: string
}
export const testBase = base.extend<{}, MyFixtures1>({
fixture1: [
async ({}, use) => {
console.log("fixture1 setup once per worker")
use("one")
console.log("fixture1 teardown once per worker")
},
{ scope: "worker" }
]
})
interface MyFixtures2 {
fixture2: string
}
export const test = testBase.extend<MyFixtures2>({
fixture2: async ({ fixture1 }, use) => {
console.log("fixture2 setup for each test")
use(`two-${fixture1}`)
console.log("fixture2 teardown for each test")
},
})
test("should allow me use composed fixture", async ({ fixture1, fixture2 }) => {
console.log(`from first fixture ${fixture1}`)
console.log(`from second fixture ${fixture2}`)
})
test("should base", async ({ fixture1 }) => {
console.log(`from first fixture ${fixture1}`)
})
test("should derived", async ({ fixture2 }) => {
console.log(`from second fixture ${fixture2}`)
})
More info about how to use fixtures in docs
I'm trying to create a playwright test (in javascript) that uses the page object model of classes, but where the test and page object model aren't in the same directory path.
The problem I'm having is it can't find my page-object-model class file. The error is Error: Cannot find module './pom/home-page'. What am I missing or doing wrong?
My file setup and path structure are as follows:
/package.config.js
...
const config = {
testDir: './test/playwright',
...
/test/playwright/pom/home-page.js
const { expect } = require ('#playwright/test');
exports.HomePage = class HomePage {
constructor(page) {
this.page = page;
this.searchInput = page.locator('#searchInput');
this.searchButton = page.locator('#searchButton');
}
}
/test/playwright/scripts/home/search.spec.js
const {test, expect} = require('#playwright/test');
const {HomePage} = require('./pom/home-page');
test.beforeAll( async ({ page }) => { ... });
test.beforeEach( async ({ page }) => { ... });
test.afterAll( async ({ page }) => { ... });
test.describe( 'As a user I want to search', () => {
test('"mySearchTerm1" and return {the expected result}', async ({ page }) => {
const homePage = new HomePage(page);
...
});
test('"mySearchTerm2" and return {the expected result}', async ({ page }) => {
const homePage = new HomePage(page);
...
});
});
Those using TypeScript can simplify this using tsconfig.json
https://playwright.dev/docs/test-typescript#manually-compile-tests-with-typescript
in tsconfig add:
"baseUrl": ".",
"paths":{
"#pages/*":[
"/test/playwright/pom/*"
]
}
Then you can import it in your fixture or test file like this:
import { HomePage } from "#pages/home-page"
This can be used to shorten fixtures or other files.
So, apparently the file reference is relative to the directory the test is located, not the testDir directory defined in the config file. I need to change line 2 in search.spec.js
const {HomePage} = require('../../pom/home-page');
Is it possible to define a browser with Javascript disabled to simulate how a crawler would view a page? There should be an option that can be passed.
You can pass javaScriptEnabled in the BrowserContext options:
const playwright = require("playwright");
(async () => {
const browser = await playwright.chromium.launch();
const context = await browser.newContext({
javaScriptEnabled: false
});
const page = await context.newPage();
// ...
await browser.close();
})();
In the case of #playwright/test (where you don't define browser.newContext yourself) you can set javaScriptEnabled in testOptions.
(1) In the playwright.config.js file:
const config = {
use: {
headless: false,
javaScriptEnabled: false
},
};
module.exports = config;
(2) or with test.use:
const { test, expect } = require('#playwright/test');
test.use({ javaScriptEnabled: false });
test('website without JavaScript', async ({ page }) => {
// ...
});
I'm using rails as backend and react native as front end, I'm trying to upload one photo using formdata in react native and using active storage in rails to save it.
using one model name Room.rb and has_one_attached :photo.
Room.rb
class Room < ApplicationRecord
has_one_attached :photo
end
here is the params received by rails, there are two (room_name and photo)
{
"room_name"=>"Guest Room",
"photo"=>
<ActionController::Parameters {
"uri"=>"file:///Users/MyName/Library/Developer/CoreSimulator/Devices/guest_room.jpg",
"name"=>"guest_room.jpg",
"type"=>"image/jpg"
} permitted: true >
}
room_controller.rb to save and receive file as follow
def create
#room = Room.create(room_params)
if #room.save
render json: RoomSerializer.new(#room).serializable_hash, status: :created
else
render json: { errors: #room.errors }, status: :unprocessable_entity
end
end
I get an error inside #room.save, saying 'TypeError - hash key "uri" is not a Symbol:'
my expectation after I choose an image from mobile phone (client) and press save button, it will automatically download an image, this is also the reason I send using FormData from react native.
Update 2:
here is part of react native that upload photo,
const preparePhoto = (uriPhoto) => {
// ImagePicker saves the taken photo to disk and returns a local URI to it
const localUri = uriPhoto;
const name = localUri.split('/').pop();
// Infer the type of the image
const match = /\.(\w+)$/.exec(name);
const type = match ? `image/${match[1]}` : `image`;
return [name, type];
};
const createRoom = dispatch => async ({ room_name, uriPhoto }) => {
const [name, type] = preparePhoto(uriPhoto);
const photo = { uri: uriPhoto, name, type };
const room = { room_name, photo };
const formData = new FormData();
formData.append('room', JSON.stringify(room));
const config = { headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
} };
try {
const response = await serverApi.post('/rooms', formData, config);
dispatch({ type: 'clear_error' });
} catch (err) {
console.log('error: ', err);
dispatch({ type: 'add_error', payload: 'Sorry we have problem' });
}
};
update 3:
source code to choose an image and send it to context
import React, { useState } from 'react';
import Constants from 'expo-constants';
import {
ActivityIndicator,
Button,
Clipboard,
Image,
Share,
StatusBar,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import * as Permissions from 'expo-permissions';
const RoomUploadPhoto = ({ uriPhoto, onPhotoChange }) => {
const [uploading, setUploading] = useState(false);
const renderUploadingIndicator = () => {
if (uploading) {
return <ActivityIndicator animating size="large" />;
}
};
const askPermission = async (type, failureMessage) => {
const { status, permissions } = await Permissions.askAsync(type);
if (status === 'denied') {
alert(failureMessage);
}
};
const handleImagePicked = (pickerResult) => {
onPhotoChange(pickerResult.uri);
};
const takePhoto = async () => {
await askPermission(
Permissions.CAMERA,
'We need the camera permission to take a picture...'
);
await askPermission(
Permissions.CAMERA_ROLL,
'We need the camera-roll permission to read pictures from your phone...'
);
const pickerResult = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
});
handleImagePicked(pickerResult);
};
const pickImage = async () => {
await askPermission(
Permissions.CAMERA_ROLL,
'We need the camera-roll permission to read pictures from your phone...'
);
const pickerResult = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [4, 3],
});
handleImagePicked(pickerResult);
};
const renderControls = () => {
if (!uploading) {
return (
<View>
<View style={styles.viewSatu}>
<Button
onPress={pickImage}
title="Pick an image from camera roll"
/>
</View>
<View style={styles.viewSatu}>
<Button onPress={takePhoto} title="Take a photo" />
</View>
</View>
);
}
};
return (
<React.Fragment>
<Text>upload photo</Text>
{renderUploadingIndicator()}
{renderControls()}
</React.Fragment>
);
};
const styles = StyleSheet.create({
viewSatu: {
marginVertical: 8
}
});
export default RoomUploadPhoto;
Make sure you post File object or base64 content to backend. Your photo is just a json object at the moment contains file path and name.
Please remove the photo param from your room_params.
def room_params
params.require(:room).permit(
:room_name
)
end
And attach your photo when you create the room:
def create
#room = Room.new(room_params)
#room.attach params[:photo]
...