Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Canvas apps run inside an iframe within the Power Apps player. This guide explains how to launch a canvas app, scope your selectors to the correct frame, and interact with controls by using data-control-name attributes.
How canvas app testing works
When a canvas app loads in play mode, the runtime hosts the app inside an iframe:
iframe[name="fullscreen-app-host"]
All controls inside the app have a data-control-name attribute that matches the control name you set in Power Apps Studio. Gallery items have data-control-part="gallery-item".
You scope all locators to this frame before interacting with controls:
const canvasFrame = page.frameLocator('iframe[name="fullscreen-app-host"]');
Launch a canvas app
Use AppProvider to launch the app and get the CanvasAppPage object:
import { test, expect } from '@playwright/test';
import {
AppProvider,
AppType,
AppLaunchMode,
buildCanvasAppUrlFromEnv,
} from 'power-platform-playwright-toolkit';
const CANVAS_APP_URL = buildCanvasAppUrlFromEnv();
test.beforeEach(async ({ page, context }) => {
const app = new AppProvider(page, context);
await app.launch({
app: 'Northwind Orders',
type: AppType.Canvas,
mode: AppLaunchMode.Play,
skipMakerPortal: true, // bypasses Power Apps navigation
directUrl: CANVAS_APP_URL,
});
});
Tip
Setting skipMakerPortal: true and providing a directUrl saves 10–20 seconds per test by bypassing Power Apps navigation.
Wait for the app to load
After launch, wait for a known control to appear before interacting:
const canvasFrame = page.frameLocator('iframe[name="fullscreen-app-host"]');
// Wait for gallery to confirm the app is loaded and data is present
await canvasFrame
.locator('[data-control-name="Gallery1"] [data-control-part="gallery-item"]')
.first()
.waitFor({ state: 'visible', timeout: 60000 });
Note
Canvas apps backed by Dataverse can take 30–60 seconds to load data into a gallery. Use a 60-second timeout for gallery selectors.
Interact with controls
The following examples show how to interact with common canvas app controls using frame-scoped locators.
Click a button
Locate a button by its data-control-name attribute, wait for it to be visible, and then click it.
const addButton = canvasFrame.locator('[data-control-name="icon3"]');
await addButton.waitFor({ state: 'visible', timeout: 10000 });
await addButton.click();
Fill a text input
Use the fill() method to set the value of a text input, targeting it by its aria-label.
const orderNumberInput = canvasFrame.locator('input[aria-label="Order Number"]');
await orderNumberInput.fill('ORD-12345');
Select a gallery item
Filter gallery items by their displayed text content to find and click a specific record.
const galleryItem = canvasFrame
.locator('[data-control-part="gallery-item"]')
.filter({ has: canvasFrame.locator('[data-control-name="Title1"]').getByText('Order 001') });
await galleryItem.waitFor({ state: 'visible' });
await galleryItem.click();
Count gallery items
Use the count() method to verify that the gallery contains the expected number of items.
const galleryItems = canvasFrame.locator('[data-control-name="Gallery1"] [data-control-part="gallery-item"]');
const count = await galleryItems.count();
expect(count).toBeGreaterThan(0);
Create a page object for your canvas app
For maintainability, encapsulate selectors and actions in a Page Object class:
// pages/my-app/MyCanvasAppPage.ts
import { Page, FrameLocator } from '@playwright/test';
export class MyCanvasAppPage {
private readonly frame: FrameLocator;
constructor(private readonly page: Page) {
this.frame = page.frameLocator('iframe[name="fullscreen-app-host"]');
}
get addButton() {
return this.frame.locator('[data-control-name="AddButton"]');
}
get gallery() {
return this.frame.locator('[data-control-name="Gallery1"]');
}
async waitForLoad(): Promise<void> {
await this.gallery
.locator('[data-control-part="gallery-item"]')
.first()
.waitFor({ state: 'visible', timeout: 60000 });
}
async clickAdd(): Promise<void> {
await this.addButton.click();
}
async getItemCount(): Promise<number> {
return this.gallery.locator('[data-control-part="gallery-item"]').count();
}
}
Full CRUD test example for canvas apps
This example combines app launch, gallery verification, and form interaction into a complete test suite.
import { test, expect, FrameLocator } from '@playwright/test';
import { AppProvider, AppType, AppLaunchMode, buildCanvasAppUrlFromEnv } from 'power-platform-playwright-toolkit';
const CANVAS_APP_URL = buildCanvasAppUrlFromEnv();
test.describe('Canvas App - Orders', () => {
let canvasFrame: FrameLocator;
test.beforeEach(async ({ page, context }) => {
const app = new AppProvider(page, context);
await app.launch({
app: 'Orders App',
type: AppType.Canvas,
mode: AppLaunchMode.Play,
skipMakerPortal: true,
directUrl: CANVAS_APP_URL,
});
canvasFrame = page.frameLocator('iframe[name="fullscreen-app-host"]');
await canvasFrame
.locator('[data-control-part="gallery-item"]')
.first()
.waitFor({ state: 'visible', timeout: 60000 });
});
test('should display orders in gallery', async () => {
const count = await canvasFrame
.locator('[data-control-part="gallery-item"]')
.count();
expect(count).toBeGreaterThan(0);
});
test('should click Add and show form', async ({ page }) => {
await canvasFrame.locator('[data-control-name="icon3"]').click();
await page.waitForTimeout(2000);
const input = canvasFrame.locator('input[type="text"]').first();
await expect(input).toBeVisible();
});
});
Discover control names in canvas apps
To find the data-control-name values in your app:
- Open the app in play mode in a browser.
- Open browser developer tools (F12).
- Use the Inspector to hover over controls and look for
data-control-nameattributes.
Alternatively, use the Playwright MCP server to ask an AI assistant to inspect the DOM and generate selectors for you. See AI-assisted testing.