@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.
@@ -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 &amp; capabilities:</b> point at the app under test with
1717
- <b>Browse…</b>, tweak or <b>+ Add</b> Appium capabilities (<b>↺ Reset</b> restores the
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
- $('screen-img').src = 'data:image/png;base64,' + j.screenshot;
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
- const sel = $('cap-device').value.trim();
4511
- if (sel) {
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
- renderDevices();
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 (<b>↺ Reset</b> restores config defaults).' },
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.25",
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": "npm run build",
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
  },