@skyramp/skyramp 1.3.9 → 1.3.11
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/package.json +5 -3
- package/scripts/build-pdf-bundle.js +141 -0
- package/scripts/pdf-execution-helpers.js +340 -0
- package/src/classes/MockV2.d.ts +9 -17
- package/src/classes/MockV2.js +20 -22
- package/src/classes/SkyrampClient.d.ts +2 -0
- package/src/classes/SkyrampClient.js +4 -3
- package/src/classes/SmartPlaywright.js +369 -13
- package/src/pdfViewer/bundle.d.ts +11 -0
- package/src/pdfViewer/bundle.js +1349 -0
- package/src/pdfViewer/index.d.ts +8 -0
- package/src/pdfViewer/index.js +14 -0
- package/src/pdfViewer/validator.d.ts +25 -0
- package/src/pdfViewer/validator.js +119 -0
- package/src/utils.d.ts +2 -2
- package/src/utils.js +14 -3
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
/* global window */
|
|
1
2
|
const { expect: playwrightExpect } = require('@playwright/test');
|
|
2
3
|
const lib = require('../lib');
|
|
3
4
|
const koffi = require('koffi');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const path = require('path');
|
|
7
|
+
const { PDF_VIEWER_INJECTION_SCRIPT } = require('../pdfViewer');
|
|
6
8
|
|
|
7
9
|
const responseType = koffi.struct({
|
|
8
10
|
response: 'char*',
|
|
@@ -290,13 +292,30 @@ class SkyrampPlaywrightLocator {
|
|
|
290
292
|
});
|
|
291
293
|
}
|
|
292
294
|
|
|
293
|
-
|
|
295
|
+
/**
|
|
296
|
+
* execute locator's actions
|
|
297
|
+
*
|
|
298
|
+
* @param {bool} reducedTimeout - it forces 3sec timeout when set to true
|
|
299
|
+
*/
|
|
300
|
+
async execute(reducedTimeout) {
|
|
294
301
|
debug(` execute ${ this._locator}.${this.execFname} ${this.execParam ?? ''} ${this.execArgs ?? ''}`)
|
|
302
|
+
var newArgs = this.execArgs
|
|
303
|
+
if (reducedTimeout !== undefined && reducedTimeout) {
|
|
304
|
+
if (this.execArgs !== null && this.execArgs !== undefined) {
|
|
305
|
+
// we can safetly assume option is the first element in the execArgs
|
|
306
|
+
newArgs = [ { ...this.execArgs[0], timeout: 3000}, ...this.execArgs.slice(1)];
|
|
307
|
+
debug(' reduce timeout', this.execArgs, newArgs)
|
|
308
|
+
} else {
|
|
309
|
+
newArgs = [{ timeout: 3000 }];
|
|
310
|
+
debug(' reduce timeout', newArgs)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
295
314
|
const func = this._locator[this.execFname];
|
|
296
315
|
if (this.execParam !== null && this.execParam !== undefined) {
|
|
297
|
-
return func.call(this._locator, this.execParam, ...
|
|
316
|
+
return func.call(this._locator, this.execParam, ...newArgs);
|
|
298
317
|
} else {
|
|
299
|
-
return func.call(this._locator, ...
|
|
318
|
+
return func.call(this._locator, ...newArgs);
|
|
300
319
|
}
|
|
301
320
|
}
|
|
302
321
|
|
|
@@ -339,7 +358,7 @@ class SkyrampPlaywrightLocator {
|
|
|
339
358
|
|
|
340
359
|
newPrevHydrationErrorMsg() {
|
|
341
360
|
return `Cannot find locator ${this._locator} and likely a hydration issue on ${this._previousLocator._locator}.\n` +
|
|
342
|
-
`Please add enough waitForTimeout() on ${this.
|
|
361
|
+
`Please add enough waitForTimeout() on ${this._previousLocator._locator}`;
|
|
343
362
|
}
|
|
344
363
|
|
|
345
364
|
newMultiLocatorErrorMsg() {
|
|
@@ -392,16 +411,16 @@ class SkyrampPlaywrightLocator {
|
|
|
392
411
|
await this.wait(defaultWaitForTimeout);
|
|
393
412
|
|
|
394
413
|
// Is this really necessary?
|
|
395
|
-
await this.execute().then(result => {
|
|
414
|
+
await this.execute(true).then(result => {
|
|
396
415
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
397
416
|
}).catch(() => {
|
|
398
417
|
debug(` failed second time and execute previous locator ${this._previousLocator._locator} again`);
|
|
399
|
-
this._previousLocator.execute();
|
|
418
|
+
this._previousLocator.execute(true);
|
|
400
419
|
}).catch(() => {
|
|
401
420
|
debug(` failed to execute previous locator ${this._previousLocator._locator} again, continue`);
|
|
402
421
|
});
|
|
403
422
|
|
|
404
|
-
return this.execute().catch(newError => {
|
|
423
|
+
return this.execute(true).catch(newError => {
|
|
405
424
|
debug(` third attempt on ${this._locator} failed ${newError.name}`);
|
|
406
425
|
if (newError.name == "TimeoutError") {
|
|
407
426
|
// this hadn't happened yet. we need to validate if this is indeed hydration case
|
|
@@ -455,7 +474,7 @@ class SkyrampPlaywrightLocator {
|
|
|
455
474
|
const previousCount = await this._previousLocator.count();
|
|
456
475
|
debug(` re-execute the previous one ${this._previousLocator._locator}, new locator count = ${previousCount}, ${currentUrl}`);
|
|
457
476
|
// re-execute the previous locator
|
|
458
|
-
await this._previousLocator.execute().catch(() => {
|
|
477
|
+
await this._previousLocator.execute(true).catch(() => {
|
|
459
478
|
// log the failure but continues to the current one
|
|
460
479
|
debug(` failed to execute previous locator ${this._previousLocator._locator} again, continue`);
|
|
461
480
|
});
|
|
@@ -470,7 +489,7 @@ class SkyrampPlaywrightLocator {
|
|
|
470
489
|
debug(` ${this._locator} failed at first try. attempting again with some timeout`);
|
|
471
490
|
// wait for some time and re execute
|
|
472
491
|
await this.wait(defaultWaitForTimeout);
|
|
473
|
-
return this.execute().then(result => {
|
|
492
|
+
return this.execute(true).then(result => {
|
|
474
493
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
475
494
|
}).catch(newError => {
|
|
476
495
|
return this._retryWithLLM(newError, this.newPrevHydrationErrorMsg());
|
|
@@ -504,7 +523,7 @@ class SkyrampPlaywrightLocator {
|
|
|
504
523
|
if (error.name == "TimeoutError") {
|
|
505
524
|
debug(`${this._locator} failed at first try. attempting again with some timeout`);
|
|
506
525
|
await this.wait(defaultWaitForTimeout);
|
|
507
|
-
return this.execute().then(result=> {
|
|
526
|
+
return this.execute(true).then(result=> {
|
|
508
527
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
509
528
|
}).catch(newError => {
|
|
510
529
|
if (newError.name == "TimeoutError") {
|
|
@@ -717,10 +736,20 @@ class SkyrampPlaywrightLocator {
|
|
|
717
736
|
|
|
718
737
|
class SkyrampPlaywrightPage {
|
|
719
738
|
constructor(page, testInfo) {
|
|
720
|
-
checkForUpdate("npm")
|
|
739
|
+
checkForUpdate("npm").catch((error) => {
|
|
740
|
+
console.error('checkForUpdate("npm") failed:', error);
|
|
741
|
+
});
|
|
721
742
|
|
|
722
743
|
this._page = page;
|
|
723
744
|
this._testInfo = testInfo; // Store testInfo for screenshot auto-baseline
|
|
745
|
+
|
|
746
|
+
// Setup PDF viewer injection (store promise to await before navigation)
|
|
747
|
+
this._pdfSetupPromise = this._setupPdfInjection().catch(err => {
|
|
748
|
+
debug('[SmartPlaywright] PDF setup failed:', err);
|
|
749
|
+
// Return resolved promise to allow test to continue even if setup fails
|
|
750
|
+
return Promise.resolve();
|
|
751
|
+
});
|
|
752
|
+
|
|
724
753
|
return new Proxy(this, {
|
|
725
754
|
// The `get` trap is the key to forwarding.
|
|
726
755
|
// This will foraward any methods not implemented in this struct
|
|
@@ -749,6 +778,280 @@ class SkyrampPlaywrightPage {
|
|
|
749
778
|
return this._page;
|
|
750
779
|
}
|
|
751
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Sets up PDF viewer injection for this page and all future popups
|
|
783
|
+
* Called automatically from constructor
|
|
784
|
+
*/
|
|
785
|
+
async _setupPdfInjection() {
|
|
786
|
+
try {
|
|
787
|
+
// Check if PDF injection is already set up for this context (prevent duplicate setup)
|
|
788
|
+
const context = this._page.context();
|
|
789
|
+
if (context.__pw_pdfInjectionSetup) {
|
|
790
|
+
debug('[SmartPlaywright] PDF injection already set up for this context, skipping');
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
debug('[SmartPlaywright] Setting up PDF viewer injection...');
|
|
795
|
+
|
|
796
|
+
// 1. Add init script at CONTEXT level (applies to ALL pages including popups)
|
|
797
|
+
await context.addInitScript(PDF_VIEWER_INJECTION_SCRIPT);
|
|
798
|
+
|
|
799
|
+
// 2. Set up route interception for PDFs at CONTEXT level
|
|
800
|
+
// This ensures it applies to all pages (including popups) immediately
|
|
801
|
+
await context.route('**/*', async (route) => {
|
|
802
|
+
const url = route.request().url();
|
|
803
|
+
const resourceType = route.request().resourceType();
|
|
804
|
+
|
|
805
|
+
// Check if this is a PDF URL (must contain .pdf in the URL)
|
|
806
|
+
const isPdfUrl = url.toLowerCase().includes('.pdf');
|
|
807
|
+
|
|
808
|
+
// Debug logging
|
|
809
|
+
if (isPdfUrl) {
|
|
810
|
+
debug(`[SmartPlaywright] PDF URL detected: ${url.substring(0, 100)}`);
|
|
811
|
+
debug(`[SmartPlaywright] Resource type: ${resourceType}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (!isPdfUrl) {
|
|
815
|
+
// Not a PDF, continue normally
|
|
816
|
+
await route.continue();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Handle PDF document navigation (direct navigation to PDF URL)
|
|
821
|
+
if (resourceType === 'document') {
|
|
822
|
+
debug('[SmartPlaywright] Intercepting PDF navigation:', url);
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
// Fetch the PDF via Playwright's context (bypasses CORS)
|
|
826
|
+
const response = await this._page.context().request.fetch(url);
|
|
827
|
+
const pdfBuffer = await response.body();
|
|
828
|
+
const base64Pdf = pdfBuffer.toString('base64');
|
|
829
|
+
const dataUrl = `data:application/pdf;base64,${base64Pdf}`;
|
|
830
|
+
|
|
831
|
+
// Extract filename from URL
|
|
832
|
+
let filename = 'Document.pdf';
|
|
833
|
+
try {
|
|
834
|
+
const urlObj = new URL(url);
|
|
835
|
+
const pathname = urlObj.pathname;
|
|
836
|
+
const lastSlash = pathname.lastIndexOf('/');
|
|
837
|
+
if (lastSlash !== -1) {
|
|
838
|
+
filename = pathname.substring(lastSlash + 1);
|
|
839
|
+
filename = decodeURIComponent(filename);
|
|
840
|
+
}
|
|
841
|
+
} catch (e) {
|
|
842
|
+
// Use default filename
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Create HTML page with PDF.js viewer
|
|
846
|
+
const html = `<!DOCTYPE html>
|
|
847
|
+
<html>
|
|
848
|
+
<head>
|
|
849
|
+
<meta charset="UTF-8">
|
|
850
|
+
<title>${filename}</title>
|
|
851
|
+
<style>
|
|
852
|
+
body, html {
|
|
853
|
+
margin: 0;
|
|
854
|
+
padding: 0;
|
|
855
|
+
width: 100%;
|
|
856
|
+
height: 100%;
|
|
857
|
+
overflow: hidden;
|
|
858
|
+
}
|
|
859
|
+
</style>
|
|
860
|
+
</head>
|
|
861
|
+
<body>
|
|
862
|
+
<div id="pw-pdf-viewer-container"></div>
|
|
863
|
+
<script>
|
|
864
|
+
// Wait for init script to load PdfJsViewer class, then render PDF directly
|
|
865
|
+
(function checkAndRender() {
|
|
866
|
+
// Check if PdfJsViewer class is available (from init script)
|
|
867
|
+
if (typeof PdfJsViewer !== 'undefined') {
|
|
868
|
+
console.log('[PW-PDF] Init script loaded, rendering PDF...');
|
|
869
|
+
|
|
870
|
+
const container = document.getElementById('pw-pdf-viewer-container');
|
|
871
|
+
container.setAttribute('data-pw-pdf-viewer', 'true');
|
|
872
|
+
container.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #525252;';
|
|
873
|
+
|
|
874
|
+
// Create PDF.js viewer instance and render
|
|
875
|
+
const viewer = new PdfJsViewer(container);
|
|
876
|
+
viewer.renderPdf({
|
|
877
|
+
pdfDataUrl: '${dataUrl}',
|
|
878
|
+
filename: '${filename}',
|
|
879
|
+
onReady: () => {
|
|
880
|
+
console.log('[PW-PDF] ✅ PDF rendered successfully');
|
|
881
|
+
},
|
|
882
|
+
onError: (error) => {
|
|
883
|
+
console.error('[PW-PDF] ❌ Failed to render PDF:', error);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
} else {
|
|
887
|
+
// PdfJsViewer not loaded yet, wait and retry
|
|
888
|
+
setTimeout(checkAndRender, 50);
|
|
889
|
+
}
|
|
890
|
+
})();
|
|
891
|
+
</script>
|
|
892
|
+
</body>
|
|
893
|
+
</html>`;
|
|
894
|
+
|
|
895
|
+
await route.fulfill({
|
|
896
|
+
status: 200,
|
|
897
|
+
headers: { 'Content-Type': 'text/html' },
|
|
898
|
+
body: html
|
|
899
|
+
});
|
|
900
|
+
} catch (error) {
|
|
901
|
+
debug('[SmartPlaywright] PDF navigation interception failed:', error.message);
|
|
902
|
+
await route.abort('failed');
|
|
903
|
+
}
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Handle PDF fetch/XHR requests (for embedded PDFs or downloads)
|
|
908
|
+
if (resourceType === 'fetch' || resourceType === 'xhr') {
|
|
909
|
+
debug('[SmartPlaywright] Intercepting PDF fetch:', url);
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
// Fetch via Playwright's context (bypasses CORS)
|
|
913
|
+
const response = await this._page.context().request.fetch(url);
|
|
914
|
+
const buffer = await response.body();
|
|
915
|
+
|
|
916
|
+
// Store the PDF URL and data for potential blob URL handling
|
|
917
|
+
await this._page.evaluate((pdfUrl) => {
|
|
918
|
+
window.__pw_lastPdfUrl = pdfUrl;
|
|
919
|
+
}, url).catch(() => {});
|
|
920
|
+
|
|
921
|
+
await route.fulfill({
|
|
922
|
+
status: 200,
|
|
923
|
+
headers: { 'Content-Type': 'application/pdf' },
|
|
924
|
+
body: buffer
|
|
925
|
+
});
|
|
926
|
+
} catch (error) {
|
|
927
|
+
debug('[SmartPlaywright] PDF fetch failed:', error.message);
|
|
928
|
+
await route.abort('failed');
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Intercept blob: URLs (likely created after PDF fetch)
|
|
934
|
+
if (url.startsWith('blob:') && resourceType === 'document') {
|
|
935
|
+
debug('[SmartPlaywright] Intercepting blob URL navigation (likely PDF)');
|
|
936
|
+
|
|
937
|
+
// Get the last PDF URL that was fetched
|
|
938
|
+
const lastPdfUrl = await this._page.evaluate(() => window.__pw_lastPdfUrl).catch(() => null);
|
|
939
|
+
|
|
940
|
+
if (lastPdfUrl) {
|
|
941
|
+
debug('[SmartPlaywright] Blob URL is for PDF, redirecting to HTML viewer');
|
|
942
|
+
|
|
943
|
+
try {
|
|
944
|
+
// Fetch the original PDF
|
|
945
|
+
const response = await this._page.context().request.fetch(lastPdfUrl);
|
|
946
|
+
const pdfBuffer = await response.body();
|
|
947
|
+
const base64Pdf = pdfBuffer.toString('base64');
|
|
948
|
+
const dataUrl = `data:application/pdf;base64,${base64Pdf}`;
|
|
949
|
+
|
|
950
|
+
let filename = 'Document.pdf';
|
|
951
|
+
try {
|
|
952
|
+
const urlObj = new URL(lastPdfUrl);
|
|
953
|
+
const pathname = urlObj.pathname;
|
|
954
|
+
const lastSlash = pathname.lastIndexOf('/');
|
|
955
|
+
if (lastSlash !== -1) {
|
|
956
|
+
filename = pathname.substring(lastSlash + 1);
|
|
957
|
+
filename = decodeURIComponent(filename);
|
|
958
|
+
}
|
|
959
|
+
} catch (e) {
|
|
960
|
+
// Use default
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const html = `<!DOCTYPE html>
|
|
964
|
+
<html>
|
|
965
|
+
<head>
|
|
966
|
+
<meta charset="UTF-8">
|
|
967
|
+
<title>${filename}</title>
|
|
968
|
+
<style>
|
|
969
|
+
body, html {
|
|
970
|
+
margin: 0;
|
|
971
|
+
padding: 0;
|
|
972
|
+
width: 100%;
|
|
973
|
+
height: 100%;
|
|
974
|
+
overflow: hidden;
|
|
975
|
+
}
|
|
976
|
+
</style>
|
|
977
|
+
</head>
|
|
978
|
+
<body>
|
|
979
|
+
<div id="pw-pdf-viewer-container"></div>
|
|
980
|
+
<script>
|
|
981
|
+
// Wait for init script to load PdfJsViewer class, then render PDF directly
|
|
982
|
+
(function checkAndRender() {
|
|
983
|
+
// Check if PdfJsViewer class is available (from init script)
|
|
984
|
+
if (typeof PdfJsViewer !== 'undefined') {
|
|
985
|
+
console.log('[PW-PDF] [BLOB] Init script loaded, rendering PDF...');
|
|
986
|
+
|
|
987
|
+
const container = document.getElementById('pw-pdf-viewer-container');
|
|
988
|
+
container.setAttribute('data-pw-pdf-viewer', 'true');
|
|
989
|
+
container.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #525252;';
|
|
990
|
+
|
|
991
|
+
// Create PDF.js viewer instance and render
|
|
992
|
+
const viewer = new PdfJsViewer(container);
|
|
993
|
+
viewer.renderPdf({
|
|
994
|
+
pdfDataUrl: '${dataUrl}',
|
|
995
|
+
filename: '${filename}',
|
|
996
|
+
onReady: () => {
|
|
997
|
+
console.log('[PW-PDF] [BLOB] ✅ PDF rendered successfully');
|
|
998
|
+
},
|
|
999
|
+
onError: (error) => {
|
|
1000
|
+
console.error('[PW-PDF] [BLOB] ❌ Failed to render PDF:', error);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
} else {
|
|
1004
|
+
// PdfJsViewer not loaded yet, wait and retry
|
|
1005
|
+
setTimeout(checkAndRender, 50);
|
|
1006
|
+
}
|
|
1007
|
+
})();
|
|
1008
|
+
</script>
|
|
1009
|
+
</body>
|
|
1010
|
+
</html>`;
|
|
1011
|
+
|
|
1012
|
+
await route.fulfill({
|
|
1013
|
+
status: 200,
|
|
1014
|
+
headers: { 'Content-Type': 'text/html' },
|
|
1015
|
+
body: html
|
|
1016
|
+
});
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
debug('[SmartPlaywright] Blob URL interception failed:', error.message);
|
|
1019
|
+
await route.continue();
|
|
1020
|
+
}
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Other PDF resource types, continue normally
|
|
1026
|
+
await route.continue();
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// 3. Set up window.open() interception on the parent page
|
|
1030
|
+
await this._page.evaluate(() => {
|
|
1031
|
+
if (window.__pw_setupPdfPopupInterception) {
|
|
1032
|
+
window.__pw_setupPdfPopupInterception();
|
|
1033
|
+
}
|
|
1034
|
+
}).catch((err) => {
|
|
1035
|
+
// Ignore errors if page is navigating - init script will handle it
|
|
1036
|
+
debug('[SmartPlaywright] window.open() interception setup skipped (page navigating):', err.message);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
// 4. Handle future popups (for debugging only)
|
|
1040
|
+
// Note: Context-level routing and init scripts automatically apply to popups
|
|
1041
|
+
this._page.on('popup', async () => {
|
|
1042
|
+
debug('[SmartPlaywright] Popup detected - context-level routing will handle PDF interception');
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Mark context as having PDF injection set up (prevents duplicate setup)
|
|
1046
|
+
context.__pw_pdfInjectionSetup = true;
|
|
1047
|
+
|
|
1048
|
+
debug('[SmartPlaywright] ✅ PDF viewer injection setup complete');
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
debug('[SmartPlaywright] ❌ PDF setup error:', error);
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
752
1055
|
pushLocator(locator) {
|
|
753
1056
|
if (this.locators == undefined ) {
|
|
754
1057
|
this.locators = [];
|
|
@@ -822,7 +1125,23 @@ class SkyrampPlaywrightPage {
|
|
|
822
1125
|
return this.newSkyrampPlaywrightLocator(originalLocator, alt, options);
|
|
823
1126
|
}
|
|
824
1127
|
|
|
1128
|
+
async waitForResponse(arg, options) {
|
|
1129
|
+
// we increase timeout for waitForResponse to 30 sec
|
|
1130
|
+
// so that in case smart selector is required
|
|
1131
|
+
var newOptions = { timeout: 60000 }
|
|
1132
|
+
if (options !== null && options !== undefined) {
|
|
1133
|
+
newOptions = { ...options, timeout: 60000 }
|
|
1134
|
+
}
|
|
1135
|
+
debug(` waitforresponse`, newOptions)
|
|
1136
|
+
return this._page.waitForResponse(arg, newOptions)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
825
1139
|
async goto(url, options) {
|
|
1140
|
+
// Ensure PDF setup is complete before navigating
|
|
1141
|
+
if (this._pdfSetupPromise) {
|
|
1142
|
+
await this._pdfSetupPromise;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
826
1145
|
const transformedUrl = transformUrlForDocker(url);
|
|
827
1146
|
const result = await this._page.goto(transformedUrl, options);
|
|
828
1147
|
const content = await this._page.content();
|
|
@@ -833,6 +1152,21 @@ class SkyrampPlaywrightPage {
|
|
|
833
1152
|
} else {
|
|
834
1153
|
debug(`javascript not detected when visiting ${this._page.url()}`);
|
|
835
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
// If navigated directly to a PDF URL, trigger PDF viewer injection
|
|
1157
|
+
if (url.toLowerCase().includes('.pdf')) {
|
|
1158
|
+
try {
|
|
1159
|
+
await this._page.evaluate(async () => {
|
|
1160
|
+
if (window.__pw_detectAndReplacePdfs) {
|
|
1161
|
+
await window.__pw_detectAndReplacePdfs();
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
debug('[SmartPlaywright] PDF viewer injected for direct navigation');
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
debug('[SmartPlaywright] PDF injection skipped:', error.message);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
836
1170
|
return result;
|
|
837
1171
|
}
|
|
838
1172
|
|
|
@@ -917,6 +1251,28 @@ class SkyrampPageAssertions {
|
|
|
917
1251
|
return await this._playwrightExpectation.toHaveScreenshot(nameOrOptions, options);
|
|
918
1252
|
}
|
|
919
1253
|
|
|
1254
|
+
// Clip coordinates are document-relative (viewport coords + scrollX/Y at record time).
|
|
1255
|
+
// Scroll to bring the clip into view at a fixed margin below the viewport top, then
|
|
1256
|
+
// convert to viewport-relative coords for page.screenshot.
|
|
1257
|
+
let adjustedOptions = options;
|
|
1258
|
+
if (options && options.clip && typeof this._actualObject.evaluate === 'function') {
|
|
1259
|
+
const clip = options.clip;
|
|
1260
|
+
// TOP_MARGIN keeps the clip below any fixed headers (e.g. PDF toolbar = 56px).
|
|
1261
|
+
const TOP_MARGIN = 100;
|
|
1262
|
+
const scrolled = await this._actualObject.evaluate(({ y, margin }) => {
|
|
1263
|
+
window.scrollTo(0, Math.max(0, y - margin));
|
|
1264
|
+
return { x: window.scrollX || window.pageXOffset || 0, y: window.scrollY || window.pageYOffset || 0 };
|
|
1265
|
+
}, { y: clip.y, margin: TOP_MARGIN });
|
|
1266
|
+
adjustedOptions = {
|
|
1267
|
+
...options,
|
|
1268
|
+
clip: {
|
|
1269
|
+
...clip,
|
|
1270
|
+
x: clip.x - scrolled.x,
|
|
1271
|
+
y: clip.y - scrolled.y,
|
|
1272
|
+
},
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
920
1276
|
// Use Playwright's official API to get snapshot path
|
|
921
1277
|
// playwright.config.js
|
|
922
1278
|
//export default {
|
|
@@ -940,14 +1296,14 @@ class SkyrampPageAssertions {
|
|
|
940
1296
|
animations: 'disabled',
|
|
941
1297
|
caret: 'hide',
|
|
942
1298
|
scale: 'css',
|
|
943
|
-
...
|
|
1299
|
+
...adjustedOptions,
|
|
944
1300
|
path: snapshotPath // Always use our computed path
|
|
945
1301
|
});
|
|
946
1302
|
debug(`Generated baseline: ${snapshotPath}`);
|
|
947
1303
|
}
|
|
948
1304
|
|
|
949
1305
|
// Baseline exists (or just created): assert normally
|
|
950
|
-
return await this._playwrightExpectation.toHaveScreenshot(nameOrOptions,
|
|
1306
|
+
return await this._playwrightExpectation.toHaveScreenshot(nameOrOptions, adjustedOptions);
|
|
951
1307
|
}
|
|
952
1308
|
}
|
|
953
1309
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Skyramp Corporation.
|
|
3
|
+
*
|
|
4
|
+
* Bundled PDF.js viewer for execution-time injection
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Self-contained PDF viewer injection script
|
|
9
|
+
* This script will be injected into pages during test execution to replace Chrome's PDF plugin with PDF.js
|
|
10
|
+
*/
|
|
11
|
+
export declare const PDF_VIEWER_INJECTION_SCRIPT: string;
|