@taqwright/taqwright 0.0.25 → 0.0.27
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/README.md +1 -1
- package/dist/bin/init.js +3 -2
- package/dist/capabilities.js +3 -0
- package/dist/inspector/ui.d.ts +1 -1
- package/dist/inspector/ui.js +102 -12
- package/package.json +3 -2
package/dist/inspector/ui.js
CHANGED
|
@@ -431,6 +431,16 @@ export const INSPECTOR_HTML = `<!doctype html>
|
|
|
431
431
|
background: #000; box-shadow: 0 6px 22px rgba(0,0,0,0.10); }
|
|
432
432
|
#screen-img { display: block; max-width: 100%; max-height: calc(100vh - 100px);
|
|
433
433
|
user-select: none; -webkit-user-drag: none; }
|
|
434
|
+
/* Graceful fallback when a snapshot fails / returns no screenshot — shown
|
|
435
|
+
instead of the browser's broken-image glyph. */
|
|
436
|
+
.screen-unavailable-msg { display: none; box-sizing: border-box; width: 300px;
|
|
437
|
+
max-width: 100%; min-height: 480px; max-height: calc(100vh - 100px);
|
|
438
|
+
flex-direction: column; align-items: center; justify-content: center; gap: 8px;
|
|
439
|
+
padding: 24px; text-align: center; color: var(--text-dim); }
|
|
440
|
+
.screen-unavailable-title { font-size: 14px; font-weight: 600; color: var(--text); }
|
|
441
|
+
.screen-unavailable-sub { font-size: 12.5px; }
|
|
442
|
+
#screen-host.screen-unavailable #screen-img { display: none; }
|
|
443
|
+
#screen-host.screen-unavailable .screen-unavailable-msg { display: flex; }
|
|
434
444
|
#highlight { position: absolute; border: 2px solid var(--accent);
|
|
435
445
|
background: rgba(9,105,218,0.12); box-shadow: 0 0 0 9999px rgba(0,0,0,0.40) inset;
|
|
436
446
|
pointer-events: none; transition: all 0.12s ease-out; }
|
|
@@ -1147,8 +1157,6 @@ export const INSPECTOR_HTML = `<!doctype html>
|
|
|
1147
1157
|
<div class="card card-caps flex">
|
|
1148
1158
|
<div class="card-head">
|
|
1149
1159
|
<h2>Capabilities</h2>
|
|
1150
|
-
<span class="grow"></span>
|
|
1151
|
-
<button class="icon" id="btn-caps-reset" title="Reset to defaults from taqwright.config.ts">↺ Reset</button>
|
|
1152
1160
|
</div>
|
|
1153
1161
|
<div class="caps-fields">
|
|
1154
1162
|
<div class="field">
|
|
@@ -1289,6 +1297,10 @@ export const INSPECTOR_HTML = `<!doctype html>
|
|
|
1289
1297
|
</div>
|
|
1290
1298
|
<div id="screen-host">
|
|
1291
1299
|
<img id="screen-img" alt="device screen" />
|
|
1300
|
+
<div class="screen-unavailable-msg">
|
|
1301
|
+
<div class="screen-unavailable-title">Device screen unavailable</div>
|
|
1302
|
+
<div class="screen-unavailable-sub">Couldn't capture the device — retrying…</div>
|
|
1303
|
+
</div>
|
|
1292
1304
|
<div id="highlight" style="display:none"></div>
|
|
1293
1305
|
<div id="screen-action-overlay" class="screen-action-overlay" aria-hidden="true">
|
|
1294
1306
|
<div class="screen-action-card">
|
|
@@ -1714,8 +1726,7 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
1714
1726
|
<code>↻ Refresh</code> the list, and <b>Start</b> a shutdown emulator (or select a
|
|
1715
1727
|
running one / a cloud device).</li>
|
|
1716
1728
|
<li><b>Step 3 — App & capabilities:</b> point at the app under test with
|
|
1717
|
-
<b>Browse…</b>, tweak or <b>+ Add</b> Appium capabilities
|
|
1718
|
-
config defaults), then <b>Connect →</b>.</li>
|
|
1729
|
+
<b>Browse…</b>, tweak or <b>+ Add</b> Appium capabilities, then <b>Connect →</b>.</li>
|
|
1719
1730
|
</ul>
|
|
1720
1731
|
</div>
|
|
1721
1732
|
</details>
|
|
@@ -2036,6 +2047,12 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
2036
2047
|
if (autoRefreshOn) scheduleNextRefresh(Math.max(base, elapsed));
|
|
2037
2048
|
}
|
|
2038
2049
|
|
|
2050
|
+
// Toggle the "device screen unavailable" fallback (shown when a snapshot
|
|
2051
|
+
// fails or returns no screenshot, so we never render a broken <img>).
|
|
2052
|
+
function setScreenUnavailable(on) {
|
|
2053
|
+
$('screen-host').classList.toggle('screen-unavailable', !!on);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2039
2056
|
async function fetchSnapshot(opts) {
|
|
2040
2057
|
const force = opts && opts.force;
|
|
2041
2058
|
if (snapshotInFlight) {
|
|
@@ -2065,7 +2082,14 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
2065
2082
|
state.sourceXml = j.source;
|
|
2066
2083
|
$('session-meta').textContent = formatSessionMeta(j.platform, j.project);
|
|
2067
2084
|
$('screen-meta').textContent = j.viewport.w + ' × ' + j.viewport.h;
|
|
2068
|
-
|
|
2085
|
+
// Only set the image when there's an actual screenshot — an empty/missing
|
|
2086
|
+
// one would render as a broken <img>; show the fallback instead.
|
|
2087
|
+
if (typeof j.screenshot === 'string' && j.screenshot.length > 0) {
|
|
2088
|
+
$('screen-img').src = 'data:image/png;base64,' + j.screenshot;
|
|
2089
|
+
setScreenUnavailable(false);
|
|
2090
|
+
} else {
|
|
2091
|
+
setScreenUnavailable(true);
|
|
2092
|
+
}
|
|
2069
2093
|
renderTree();
|
|
2070
2094
|
if (hierarchyMode === 'xml') refreshHierarchyXml();
|
|
2071
2095
|
if (prevXpath && prevSig) {
|
|
@@ -2089,6 +2113,7 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
2089
2113
|
setStatus('idle');
|
|
2090
2114
|
} catch (err) {
|
|
2091
2115
|
setStatus('error: ' + err.message);
|
|
2116
|
+
setScreenUnavailable(true);
|
|
2092
2117
|
} finally {
|
|
2093
2118
|
snapshotInFlight = false;
|
|
2094
2119
|
}
|
|
@@ -2716,6 +2741,10 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
2716
2741
|
};
|
|
2717
2742
|
}
|
|
2718
2743
|
|
|
2744
|
+
// A corrupt/truncated data URI fails to decode — fall back rather than show
|
|
2745
|
+
// the browser's broken-image glyph.
|
|
2746
|
+
$('screen-img').addEventListener('error', () => setScreenUnavailable(true));
|
|
2747
|
+
|
|
2719
2748
|
$('screen-img').addEventListener('mouseup', (ev) => {
|
|
2720
2749
|
const pt = imgToDevice(ev);
|
|
2721
2750
|
// Pick mode (Record tab) takes priority — consume one click then dismiss.
|
|
@@ -4042,7 +4071,6 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
4042
4071
|
$('btn-appium-recheck').onclick = refreshAppiumPill;
|
|
4043
4072
|
$('btn-appium-restart').onclick = restartAppium;
|
|
4044
4073
|
$('btn-appium-start').onclick = startAppium;
|
|
4045
|
-
$('btn-caps-reset').onclick = () => applyCapsToForm(initial.defaults.capabilities);
|
|
4046
4074
|
$('btn-connect').onclick = doConnect;
|
|
4047
4075
|
$('btn-add-cap').onclick = () => addExtraRow({ key: '', value: '' }, true);
|
|
4048
4076
|
$('btn-devices-refresh').onclick = loadDevices;
|
|
@@ -4250,8 +4278,24 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
4250
4278
|
updateConnectSummary();
|
|
4251
4279
|
}
|
|
4252
4280
|
|
|
4281
|
+
// Whether the wizard is allowed to advance forward off the given step (its
|
|
4282
|
+
// prerequisites are met). Mirrors the gating in updateConnectSummary.
|
|
4283
|
+
function canAdvanceFrom(step) {
|
|
4284
|
+
if (step === 1) {
|
|
4285
|
+
return isCloudMode() ? cloudCredsValid : $('appium-pill').classList.contains('live');
|
|
4286
|
+
}
|
|
4287
|
+
// Require an actual selected, booted device — not just a pre-filled
|
|
4288
|
+
// cap-device value (config defaults seed it, which would wrongly enable Next).
|
|
4289
|
+
if (step === 2) return selectedDeviceKey !== null;
|
|
4290
|
+
return true;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4253
4293
|
function goToStep(n) {
|
|
4254
4294
|
if (n < 1 || n > 3) return;
|
|
4295
|
+
// Hard-gate forward navigation: never advance past a step whose
|
|
4296
|
+
// prerequisites aren't met — even for programmatic callers like the guided
|
|
4297
|
+
// tour. Backward navigation and re-selecting the current step are free.
|
|
4298
|
+
if (n > wizardStep && !canAdvanceFrom(wizardStep)) return;
|
|
4255
4299
|
wizardStep = n;
|
|
4256
4300
|
document.querySelectorAll('.wizard-page').forEach((p) => {
|
|
4257
4301
|
p.classList.toggle('active', Number(p.getAttribute('data-page')) === n);
|
|
@@ -4507,8 +4551,10 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
4507
4551
|
return;
|
|
4508
4552
|
}
|
|
4509
4553
|
if (wizardStep === 2) {
|
|
4510
|
-
|
|
4511
|
-
|
|
4554
|
+
// Gate on the real selection (a tapped, booted device), not the pre-filled
|
|
4555
|
+
// cap-device value — otherwise Next is enabled before any live device is picked.
|
|
4556
|
+
if (selectedDeviceKey !== null) {
|
|
4557
|
+
const sel = $('cap-device').value.trim();
|
|
4512
4558
|
summary.innerHTML =
|
|
4513
4559
|
'Selected <strong>' + escapeHtml(sel) + '</strong> — click <strong>Next</strong> or pick another device.';
|
|
4514
4560
|
nextBtn.disabled = false;
|
|
@@ -4673,6 +4719,20 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
4673
4719
|
function renderDevices() {
|
|
4674
4720
|
const data = lastDeviceData;
|
|
4675
4721
|
|
|
4722
|
+
// Drop a stale selection: if the selected device is no longer booted (e.g.
|
|
4723
|
+
// it was stopped, or shut down between polls), clear it so Next disables —
|
|
4724
|
+
// a selection must always point at a currently-live device.
|
|
4725
|
+
if (selectedDeviceKey !== null) {
|
|
4726
|
+
const all = [...(data.android || []), ...(data.ios || [])];
|
|
4727
|
+
const stillLive = all.some((d) => d.state === 'booted' && bootingKey(d) === selectedDeviceKey);
|
|
4728
|
+
if (!stillLive) {
|
|
4729
|
+
selectedDeviceKey = null;
|
|
4730
|
+
selectedCloudDevice = null;
|
|
4731
|
+
$('cap-device').value = '';
|
|
4732
|
+
updateConnectSummary();
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4676
4736
|
// Tool-missing warnings.
|
|
4677
4737
|
const warns = [];
|
|
4678
4738
|
if (data.toolsMissing?.adb) warns.push("adb not on PATH — Android emulators won't show.");
|
|
@@ -4886,7 +4946,9 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
4886
4946
|
const found = list.find((d) => bootingKey(d) === key);
|
|
4887
4947
|
if (found && found.state === 'booted') {
|
|
4888
4948
|
bootingDevices.delete(key);
|
|
4889
|
-
|
|
4949
|
+
// Auto-select the device the user just started — no manual click needed.
|
|
4950
|
+
// selectDevice() also re-renders (✓) and enables Next (gated on selection).
|
|
4951
|
+
selectDevice(found);
|
|
4890
4952
|
showToast(dev.name + ' is up and ready.', 'success', { title: 'Device booted' });
|
|
4891
4953
|
return;
|
|
4892
4954
|
}
|
|
@@ -5188,6 +5250,34 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
5188
5250
|
const t = document.querySelector('.tab[data-tab="' + name + '"]');
|
|
5189
5251
|
if (t) t.click();
|
|
5190
5252
|
}
|
|
5253
|
+
// Live tour only: the Locators / Attributes panels are empty until an element
|
|
5254
|
+
// is selected, so the tour would spotlight a blank panel. If the user hasn't
|
|
5255
|
+
// selected anything yet, auto-select a representative node (one with an id /
|
|
5256
|
+
// text / content-desc, else the first node) so those steps show real content.
|
|
5257
|
+
function tourEnsureSelection() {
|
|
5258
|
+
if (state.selected) return;
|
|
5259
|
+
let pick = null;
|
|
5260
|
+
for (const [, el] of state.nodeMap) {
|
|
5261
|
+
if (!el || !el.getAttribute) continue;
|
|
5262
|
+
if (
|
|
5263
|
+
el.getAttribute('resource-id') ||
|
|
5264
|
+
el.getAttribute('text') ||
|
|
5265
|
+
el.getAttribute('content-desc') ||
|
|
5266
|
+
el.getAttribute('name') ||
|
|
5267
|
+
el.getAttribute('label')
|
|
5268
|
+
) {
|
|
5269
|
+
pick = el;
|
|
5270
|
+
break;
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
if (!pick) {
|
|
5274
|
+
for (const [, el] of state.nodeMap) {
|
|
5275
|
+
pick = el;
|
|
5276
|
+
break;
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
if (pick) selectElement(pick);
|
|
5280
|
+
}
|
|
5191
5281
|
// Switch the demo stage's mock right-hand tab (Record / Script / Locators / Attributes).
|
|
5192
5282
|
function showDemoTab(name) {
|
|
5193
5283
|
['rec', 'script', 'loc', 'attrs'].forEach((k) => {
|
|
@@ -5210,7 +5300,7 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
5210
5300
|
{ sel: '#btn-devices-refresh', before: function () { goToStep(2); }, title: 'Step 2 — Pick a device',
|
|
5211
5301
|
body: 'Switch the <b>Android / iOS</b> tabs and <b>↻ Refresh</b> the list. <b>Start</b> a shutdown emulator, or pick a running one / a cloud device.' },
|
|
5212
5302
|
{ sel: '#btn-app-browse', before: function () { goToStep(3); }, title: 'Step 3 — App & capabilities',
|
|
5213
|
-
body: 'Point at the app under test with <b>Browse…</b>, then tweak or <b>+ Add</b> Appium capabilities
|
|
5303
|
+
body: 'Point at the app under test with <b>Browse…</b>, then tweak or <b>+ Add</b> Appium capabilities.' },
|
|
5214
5304
|
{ sel: '#btn-connect', before: function () { goToStep(3); }, title: 'Connect',
|
|
5215
5305
|
body: 'Hit <b>Connect →</b> to open the session and enter the inspector.' },
|
|
5216
5306
|
{ sel: null, title: 'You are set',
|
|
@@ -5230,9 +5320,9 @@ await mobile.getByUiSelector('new UiSelector().description("Login")').click();</
|
|
|
5230
5320
|
body: 'Press <b>Start record</b>, select an element, then choose an action — Click, Type, Clear, gestures… The <b>Actions / Screen / Assertions</b> sub-tabs switch what you capture. Each step is appended live.' },
|
|
5231
5321
|
{ sel: '#tab-script', before: function () { tourClickTab('script'); }, title: 'Recorded script',
|
|
5232
5322
|
body: 'Your test in <b>Taqwright</b> (runnable), or <b>Python</b> / <b>Java</b> (steps only). Use <b>⎘ Copy</b>, <b>↓ Export</b> (saves into your tests folder), or Clear.' },
|
|
5233
|
-
{ sel: '#tab-locators', before: function () { tourClickTab('locators'); }, title: 'Locators',
|
|
5323
|
+
{ sel: '#tab-locators', before: function () { tourEnsureSelection(); tourClickTab('locators'); }, title: 'Locators',
|
|
5234
5324
|
body: 'Ranked, uniqueness-verified selectors for the selected element — id, accessibility id, UIAutomator / NSPredicate / Class Chain, xpath. The <b>recommended</b> pick is on top; click any to copy.' },
|
|
5235
|
-
{ sel: '#tab-attrs', before: function () { tourClickTab('attrs'); }, title: 'Attributes',
|
|
5325
|
+
{ sel: '#tab-attrs', before: function () { tourEnsureSelection(); tourClickTab('attrs'); }, title: 'Attributes',
|
|
5236
5326
|
body: 'The selected element\\'s full attribute set (resource-id, class, text, content-desc, bounds…) plus its xpath.' },
|
|
5237
5327
|
{ sel: '#btn-disconnect', before: function () { tourClickTab('record'); }, title: 'Done',
|
|
5238
5328
|
body: 'When finished, <b>Disconnect</b> ends the session and returns to setup. Reopen this tour any time with <b>? Help</b>.' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taqwright/taqwright",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "E2E mobile testing on the Playwright runner",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
"format:check": "prettier --check .",
|
|
41
41
|
"test": "npm run build && node --test test/*.test.js",
|
|
42
42
|
"test:watch": "node --test --watch test/*.test.js",
|
|
43
|
+
"test:e2e": "playwright test --config e2e/playwright.config.ts",
|
|
43
44
|
"test:coverage": "npm run build && node --test --experimental-test-coverage test/*.test.js",
|
|
44
|
-
"prepare": "
|
|
45
|
+
"prepare": "node scripts/prepare.mjs",
|
|
45
46
|
"prepublishOnly": "npm run build",
|
|
46
47
|
"version": "node scripts/sync-readme-version.mjs && git add README.md"
|
|
47
48
|
},
|