please-test 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +37 -0
- package/lib/index.js +62 -0
- package/lib/interaction.js +55 -0
- package/lib/locator.js +50 -0
- package/lib/navigation.js +24 -0
- package/lib/query.js +17 -0
- package/lib/screenshot.js +16 -0
- package/package.json +49 -0
- package/readme.md +103 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Page, Locator } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export interface PageTarget {
|
|
4
|
+
url: string
|
|
5
|
+
title?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default class Please {
|
|
9
|
+
constructor(page: Page)
|
|
10
|
+
|
|
11
|
+
// ── Locator ────────────────────────────────────────────────────────────────
|
|
12
|
+
detectLocator(selector: string): string
|
|
13
|
+
toLocator(selector: string): Locator
|
|
14
|
+
|
|
15
|
+
// ── Navigasi ───────────────────────────────────────────────────────────────
|
|
16
|
+
goto(target: PageTarget): Promise<void>
|
|
17
|
+
verifyPage(target: PageTarget): Promise<void>
|
|
18
|
+
url(): Promise<string>
|
|
19
|
+
title(): Promise<string>
|
|
20
|
+
|
|
21
|
+
// ── Tunggu & Interaksi ─────────────────────────────────────────────────────
|
|
22
|
+
untilShow(label: string, selector: string, timeout?: number): Promise<void>
|
|
23
|
+
wait(ms?: number): Promise<void>
|
|
24
|
+
click(label: string, selector: string, delay?: number): Promise<void>
|
|
25
|
+
fill(label: string, selector: string, value: string): Promise<void>
|
|
26
|
+
fillAndEnter(label: string, selector: string, value: string): Promise<void>
|
|
27
|
+
clear(label: string, selector: string): Promise<void>
|
|
28
|
+
scrollTo(label: string, selector: string): Promise<void>
|
|
29
|
+
uploadFile(label: string, selector: string, filePath: string): Promise<void>
|
|
30
|
+
datepicker(label: string, selector: string, value: string): Promise<void>
|
|
31
|
+
|
|
32
|
+
// ── Baca Nilai & Assert ────────────────────────────────────────────────────
|
|
33
|
+
see(label: string, selector: string, expected?: string, timeout?: number): Promise<string>
|
|
34
|
+
|
|
35
|
+
// ── Screenshot ─────────────────────────────────────────────────────────────
|
|
36
|
+
screenshot(label?: string): Promise<string>
|
|
37
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { detectLocator, toLocator } = require('./locator')
|
|
2
|
+
const { goto, verifyPage } = require('./navigation')
|
|
3
|
+
const { untilShow, wait, click, fill, fillAndEnter, clear, scrollTo, uploadFile, datepicker } = require('./interaction')
|
|
4
|
+
const { see } = require('./query')
|
|
5
|
+
const { screenshot } = require('./screenshot')
|
|
6
|
+
|
|
7
|
+
/** @typedef {{ url: string, title?: string }} PageInfo */
|
|
8
|
+
|
|
9
|
+
class Please {
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('@playwright/test').Page} page
|
|
12
|
+
* @param {import('@playwright/test').TestType<any,any>} [test]
|
|
13
|
+
*/
|
|
14
|
+
constructor(page, test) {
|
|
15
|
+
this.page = page
|
|
16
|
+
this._step = test ? (name, fn) => test.step(name, fn) : (name, fn) => fn()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Locator ────────────────────────────────────────────────────────────────
|
|
20
|
+
/** @param {string} selector */
|
|
21
|
+
detectLocator(selector) { return detectLocator(selector) }
|
|
22
|
+
/** @param {string} selector */
|
|
23
|
+
toLocator(selector) { return toLocator(this.page, selector) }
|
|
24
|
+
|
|
25
|
+
// ── Navigasi ───────────────────────────────────────────────────────────────
|
|
26
|
+
/** @param {PageInfo} expected */
|
|
27
|
+
async goto(expected) { return this._step(`Go to "${expected.url}"`, () => goto(this.page, expected)) }
|
|
28
|
+
/** @param {PageInfo} expected */
|
|
29
|
+
async verifyPage(expected) { return this._step(`Verify page "${expected.url ?? expected.title}"`, () => verifyPage(this.page, expected)) }
|
|
30
|
+
async url() { return this.page.url() }
|
|
31
|
+
async title() { return this.page.title() }
|
|
32
|
+
|
|
33
|
+
// ── Tunggu & Interaksi ─────────────────────────────────────────────────────
|
|
34
|
+
/** @param {string} label @param {string} selector @param {number} [time] */
|
|
35
|
+
async untilShow(label, selector, time) { return this._step(`Wait "${label}"`, () => untilShow(this.page, label, selector, time)) }
|
|
36
|
+
/** @param {number} [ms] */
|
|
37
|
+
async wait(ms) { return wait(this.page, ms) }
|
|
38
|
+
/** @param {string} label @param {string} selector @param {number} [time] */
|
|
39
|
+
async click(label, selector, time) { return this._step(`Click "${label}"`, () => click(this.page, label, selector, time)) }
|
|
40
|
+
/** @param {string} label @param {string} selector @param {string} value */
|
|
41
|
+
async fill(label, selector, value) { return this._step(`Fill "${label}"`, () => fill(this.page, label, selector, value)) }
|
|
42
|
+
/** @param {string} label @param {string} selector @param {string} value */
|
|
43
|
+
async fillAndEnter(label, selector, value) { return this._step(`Fill and enter "${label}"`, () => fillAndEnter(this.page, label, selector, value)) }
|
|
44
|
+
/** @param {string} label @param {string} selector */
|
|
45
|
+
async clear(label, selector) { return this._step(`Clear "${label}"`, () => clear(this.page, label, selector)) }
|
|
46
|
+
/** @param {string} label @param {string} selector */
|
|
47
|
+
async scrollTo(label, selector) { return this._step(`Scroll to "${label}"`, () => scrollTo(this.page, label, selector)) }
|
|
48
|
+
/** @param {string} label @param {string} selector @param {string} filePath */
|
|
49
|
+
async uploadFile(label, selector, filePath) { return this._step(`Upload "${label}"`, () => uploadFile(this.page, label, selector, filePath)) }
|
|
50
|
+
/** @param {string} label @param {string} selector @param {string} value */
|
|
51
|
+
async datepicker(label, selector, value) { return this._step(`Datepicker "${label}"`, () => datepicker(this.page, label, selector, value)) }
|
|
52
|
+
|
|
53
|
+
// ── Baca Nilai & Assert ────────────────────────────────────────────────────
|
|
54
|
+
/** @param {string} label @param {string} selector @param {string} [expected] @param {number} [time] */
|
|
55
|
+
async see(label, selector, expected, time) { return this._step(`See "${label}"`, () => see(this.page, label, selector, expected, time)) }
|
|
56
|
+
|
|
57
|
+
// ── Screenshot ─────────────────────────────────────────────────────────────
|
|
58
|
+
/** @param {string} [label] */
|
|
59
|
+
async screenshot(label) { return screenshot(this.page, label) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = Please
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { toLocator } = require('./locator')
|
|
2
|
+
|
|
3
|
+
async function untilShow(page, label, selector, time = 20000) {
|
|
4
|
+
try {
|
|
5
|
+
await toLocator(page, selector).waitFor({ state: 'visible', timeout: time })
|
|
6
|
+
} catch {
|
|
7
|
+
const err = new Error(`"${label}" tidak muncul setelah ${time / 1000} detik`)
|
|
8
|
+
err.stack = err.message
|
|
9
|
+
throw err
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function wait(page, ms = 2000) {
|
|
14
|
+
await page.waitForTimeout(ms)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function click(page, label, selector, time = undefined) {
|
|
18
|
+
if (time !== undefined) await wait(page, time)
|
|
19
|
+
await untilShow(page, label, selector)
|
|
20
|
+
await toLocator(page, selector).scrollIntoViewIfNeeded()
|
|
21
|
+
await toLocator(page, selector).click()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function fill(page, label, selector, value) {
|
|
25
|
+
await untilShow(page, label, selector)
|
|
26
|
+
await toLocator(page, selector).fill(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fillAndEnter(page, label, selector, value) {
|
|
30
|
+
await untilShow(page, label, selector)
|
|
31
|
+
const el = toLocator(page, selector)
|
|
32
|
+
await el.fill(value)
|
|
33
|
+
await el.press('Enter')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function clear(page, label, selector) {
|
|
37
|
+
await untilShow(page, label, selector)
|
|
38
|
+
await toLocator(page, selector).clear()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function scrollTo(page, label, selector) {
|
|
42
|
+
await untilShow(page, label, selector)
|
|
43
|
+
await toLocator(page, selector).scrollIntoViewIfNeeded()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function uploadFile(page, label, selector, filePath) {
|
|
47
|
+
await untilShow(page, label, selector)
|
|
48
|
+
await toLocator(page, selector).setInputFiles(filePath)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function datepicker(page, label, selector, value) {
|
|
52
|
+
await fillAndEnter(page, label, selector, value)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { untilShow, wait, click, fill, fillAndEnter, clear, scrollTo, uploadFile, datepicker }
|
package/lib/locator.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const ARIA_ROLES = new Set([
|
|
2
|
+
'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote',
|
|
3
|
+
'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox',
|
|
4
|
+
'complementary', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory',
|
|
5
|
+
'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid', 'gridcell',
|
|
6
|
+
'group', 'heading', 'img', 'insertion', 'link', 'list', 'listbox', 'listitem',
|
|
7
|
+
'log', 'main', 'marquee', 'math', 'menu', 'menubar', 'menuitem', 'menuitemcheckbox',
|
|
8
|
+
'menuitemradio', 'meter', 'navigation', 'none', 'note', 'option', 'paragraph',
|
|
9
|
+
'presentation', 'progressbar', 'radio', 'radiogroup', 'region', 'row', 'rowgroup',
|
|
10
|
+
'rowheader', 'scrollbar', 'search', 'searchbox', 'separator', 'slider', 'spinbutton',
|
|
11
|
+
'status', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist',
|
|
12
|
+
'tabpanel', 'term', 'textbox', 'timer', 'toolbar', 'tooltip', 'tree', 'treegrid',
|
|
13
|
+
'treeitem',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
function detectLocator(selector) {
|
|
17
|
+
if (selector.startsWith('//') || selector.startsWith('(//'))
|
|
18
|
+
return `xpath=${selector}`
|
|
19
|
+
if (selector.startsWith('#'))
|
|
20
|
+
return selector
|
|
21
|
+
if (selector.startsWith('.') || selector.startsWith('[') || /[\s>:+~]/.test(selector) || /[.#\[:]/.test(selector))
|
|
22
|
+
return selector
|
|
23
|
+
if (/^(a|abbr|address|article|aside|audio|b|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|data|datalist|dd|del|details|dfn|dialog|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|head|header|hr|html|i|iframe|img|input|ins|kbd|label|legend|li|link|main|map|mark|menu|meta|meter|nav|noscript|object|ol|optgroup|option|output|p|picture|pre|progress|q|rp|rt|ruby|s|samp|script|section|select|small|source|span|strong|style|sub|summary|sup|table|tbody|td|template|textarea|tfoot|th|thead|time|title|tr|track|u|ul|var|video|wbr)$/.test(selector))
|
|
24
|
+
return selector
|
|
25
|
+
throw new Error(`Selector "${selector}" tidak dapat dikenali. Gunakan: #id, .class, tag, xpath (//), CSS, text=, label=, placeholder=, alt=, title=, testid=, atau shorthand button=Name.`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toLocator(page, selector) {
|
|
29
|
+
const eq = selector.indexOf('=')
|
|
30
|
+
if (eq !== -1) {
|
|
31
|
+
const prefix = selector.slice(0, eq)
|
|
32
|
+
const value = selector.slice(eq + 1)
|
|
33
|
+
if (prefix === 'role') {
|
|
34
|
+
const nameMatch = value.match(/^(\w+)\[name=(.+)\]$/)
|
|
35
|
+
return nameMatch
|
|
36
|
+
? page.getByRole(nameMatch[1], { name: nameMatch[2] })
|
|
37
|
+
: page.getByRole(value)
|
|
38
|
+
}
|
|
39
|
+
if (ARIA_ROLES.has(prefix)) return page.getByRole(prefix, { name: value })
|
|
40
|
+
if (prefix === 'label') return page.getByLabel(value)
|
|
41
|
+
if (prefix === 'text') return page.getByText(value)
|
|
42
|
+
if (prefix === 'placeholder') return page.getByPlaceholder(value)
|
|
43
|
+
if (prefix === 'alt') return page.getByAltText(value)
|
|
44
|
+
if (prefix === 'title') return page.getByTitle(value)
|
|
45
|
+
if (prefix === 'testid') return page.getByTestId(value)
|
|
46
|
+
}
|
|
47
|
+
return page.locator(detectLocator(selector))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { detectLocator, toLocator }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
async function goto(page, { url, title }) {
|
|
2
|
+
await page.goto(url)
|
|
3
|
+
if (title) {
|
|
4
|
+
const actual = await page.title()
|
|
5
|
+
if (actual !== title)
|
|
6
|
+
throw new Error(`Title tidak sesuai. Expected: "${title}", actual: "${actual}"`)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function verifyPage(page, { url, title }) {
|
|
11
|
+
await page.waitForLoadState('domcontentloaded')
|
|
12
|
+
if (url) {
|
|
13
|
+
const actual = page.url()
|
|
14
|
+
if (!actual.includes(url))
|
|
15
|
+
throw new Error(`URL tidak sesuai. Expected mengandung: "${url}", actual: "${actual}"`)
|
|
16
|
+
}
|
|
17
|
+
if (title) {
|
|
18
|
+
const actual = await page.title()
|
|
19
|
+
if (actual !== title)
|
|
20
|
+
throw new Error(`Title tidak sesuai. Expected: "${title}", actual: "${actual}"`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { goto, verifyPage }
|
package/lib/query.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { toLocator } = require('./locator')
|
|
2
|
+
const { untilShow, wait } = require('./interaction')
|
|
3
|
+
|
|
4
|
+
async function see(page, label, selector, expected = undefined, time = undefined) {
|
|
5
|
+
await untilShow(page, label, selector)
|
|
6
|
+
if (time !== undefined) await wait(page, time)
|
|
7
|
+
const el = toLocator(page, selector)
|
|
8
|
+
const tag = await el.evaluate(node => node.tagName.toLowerCase())
|
|
9
|
+
const actual = ['input', 'textarea', 'select'].includes(tag)
|
|
10
|
+
? await el.inputValue()
|
|
11
|
+
: await el.innerText()
|
|
12
|
+
if (expected !== undefined && actual !== expected)
|
|
13
|
+
throw new Error(`[${label}] konten tidak sesuai\n Expected: "${expected}"\n Received: "${actual}"`)
|
|
14
|
+
return actual
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { see }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
|
|
4
|
+
async function screenshot(page, label) {
|
|
5
|
+
const dir = path.resolve('screenshots')
|
|
6
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
7
|
+
const datetime = new Date().toISOString().replace(/[:.]/g, '-')
|
|
8
|
+
const slug = label
|
|
9
|
+
? `${label.replace(/[^a-zA-Z0-9_-]/g, '_')}_${datetime}`
|
|
10
|
+
: datetime
|
|
11
|
+
const filePath = path.join(dir, `${slug}.png`)
|
|
12
|
+
await page.screenshot({ path: filePath })
|
|
13
|
+
return filePath
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { screenshot }
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "please-test",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Shorthand helper for @playwright/test — expressive locators, readable error messages",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test:unit": "node test/unit.test.js && node test/screenshot.test.js",
|
|
9
|
+
"test:login": "playwright test test/login.spec.js",
|
|
10
|
+
"test:types": "tsd",
|
|
11
|
+
"test": "playwright test test/smoke.test.js"
|
|
12
|
+
},
|
|
13
|
+
"tsd": {
|
|
14
|
+
"entrypoint": "lib/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ghany-ersa/please-test.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"playwright",
|
|
22
|
+
"automation",
|
|
23
|
+
"testing",
|
|
24
|
+
"e2e",
|
|
25
|
+
"shorthand"
|
|
26
|
+
],
|
|
27
|
+
"author": "ghany-ersa <ghanyersa24@gmail.com>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@playwright/test": ">=1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=14.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib/index.js",
|
|
37
|
+
"lib/index.d.ts",
|
|
38
|
+
"lib/interaction.js",
|
|
39
|
+
"lib/locator.js",
|
|
40
|
+
"lib/navigation.js",
|
|
41
|
+
"lib/query.js",
|
|
42
|
+
"lib/screenshot.js"
|
|
43
|
+
],
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@playwright/test": "^1.61.0",
|
|
46
|
+
"tsd": "^0.33.0",
|
|
47
|
+
"typescript": "^6.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# please-test
|
|
2
|
+
|
|
3
|
+
Shorthand helper untuk `@playwright/test` — locator ekspresif dan pesan error yang deskriptif.
|
|
4
|
+
|
|
5
|
+
`page` datang dari fixture `@playwright/test`, please-test hanya menyediakan shorthand di atasnya.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Instalasi
|
|
10
|
+
|
|
11
|
+
Pastikan Node.js >= 14 sudah terinstall.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install please-test @playwright/test && npx playwright install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const { test } = require('@playwright/test')
|
|
19
|
+
const Please = require('please-test')
|
|
20
|
+
|
|
21
|
+
test('login berhasil', async ({ page }) => {
|
|
22
|
+
const please = new Please(page)
|
|
23
|
+
|
|
24
|
+
await please.goto({ url: 'https://myapp.com/login' })
|
|
25
|
+
await please.fill('Username', '#username', 'student')
|
|
26
|
+
await please.fill('Password', '#password', 'secret')
|
|
27
|
+
await please.click('Tombol Login', 'button=Login')
|
|
28
|
+
await please.see('Pesan Sukses', 'h1', 'Dashboard')
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
`please` adalah instance dari `new Please(page)`.
|
|
37
|
+
|
|
38
|
+
### Navigasi
|
|
39
|
+
|
|
40
|
+
| Method | Deskripsi |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `goto({ url, title? })` | Navigasi ke URL; jika `title` diberikan, validasi title halaman |
|
|
43
|
+
| `verifyPage({ url?, title? })` | Verifikasi URL dan/atau title halaman saat ini |
|
|
44
|
+
| `url()` | Ambil URL halaman saat ini |
|
|
45
|
+
| `title()` | Ambil title halaman saat ini |
|
|
46
|
+
|
|
47
|
+
### Interaksi
|
|
48
|
+
|
|
49
|
+
| Method | Deskripsi |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `click(label, selector, time?)` | Klik elemen; `time` (ms) menunda klik setelah jeda |
|
|
52
|
+
| `fill(label, selector, value)` | Isi input field |
|
|
53
|
+
| `fillAndEnter(label, selector, value)` | Isi input dan tekan Enter |
|
|
54
|
+
| `clear(label, selector)` | Kosongkan input field |
|
|
55
|
+
| `scrollTo(label, selector)` | Scroll ke elemen |
|
|
56
|
+
| `uploadFile(label, selector, path)` | Upload file via `input[type=file]` |
|
|
57
|
+
| `datepicker(label, selector, value)` | Isi input datepicker |
|
|
58
|
+
|
|
59
|
+
### Baca & Assert
|
|
60
|
+
|
|
61
|
+
| Method | Deskripsi |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `see(label, selector, expected?, time?)` | Baca konten elemen. Jika `expected` diberikan, throw jika tidak cocok — tetap mengembalikan nilai aktual |
|
|
64
|
+
|
|
65
|
+
`see` otomatis membaca `value` untuk `input`/`textarea`/`select`, dan `innerText` untuk elemen lain.
|
|
66
|
+
|
|
67
|
+
### Tunggu
|
|
68
|
+
|
|
69
|
+
| Method | Deskripsi |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `untilShow(label, selector, time?)` | Tunggu elemen muncul (default 20 detik) |
|
|
72
|
+
| `wait(ms?)` | Jeda eksplisit (default 2000ms) |
|
|
73
|
+
|
|
74
|
+
### Screenshot
|
|
75
|
+
|
|
76
|
+
| Method | Deskripsi |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `screenshot(label?)` | Simpan screenshot ke `screenshots/label_datetime.png` |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Selector
|
|
83
|
+
|
|
84
|
+
Semua method yang menerima `selector` otomatis mendeteksi tipe locator:
|
|
85
|
+
|
|
86
|
+
| Format | Contoh |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `#id` | `#username` |
|
|
89
|
+
| `text=` | `text=Klik di sini` |
|
|
90
|
+
| `role=` | `role=button[name=Submit]` |
|
|
91
|
+
| `label=` | `label=Email` |
|
|
92
|
+
| `role=Name` (shorthand) | `button=Login`, `link=Masuk`, `checkbox=Setuju` |
|
|
93
|
+
| CSS selector | `.btn-primary`, `[data-id="x"]` |
|
|
94
|
+
| XPath | `//button[@type="submit"]` |
|
|
95
|
+
| Tag HTML | `h1`, `textarea` |
|
|
96
|
+
|
|
97
|
+
> Shorthand `role=Name` didukung untuk semua ARIA role yang valid di Playwright: `button=Login`, `link=Masuk`, `checkbox=Setuju`, `textbox=Email`, `combobox=Negara`, dst.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Lisensi
|
|
102
|
+
|
|
103
|
+
MIT
|