@skyramp/skyramp 1.3.10 → 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.
@@ -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*',
@@ -740,6 +742,14 @@ class SkyrampPlaywrightPage {
740
742
 
741
743
  this._page = page;
742
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
+
743
753
  return new Proxy(this, {
744
754
  // The `get` trap is the key to forwarding.
745
755
  // This will foraward any methods not implemented in this struct
@@ -768,6 +778,280 @@ class SkyrampPlaywrightPage {
768
778
  return this._page;
769
779
  }
770
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
+
771
1055
  pushLocator(locator) {
772
1056
  if (this.locators == undefined ) {
773
1057
  this.locators = [];
@@ -853,6 +1137,11 @@ class SkyrampPlaywrightPage {
853
1137
  }
854
1138
 
855
1139
  async goto(url, options) {
1140
+ // Ensure PDF setup is complete before navigating
1141
+ if (this._pdfSetupPromise) {
1142
+ await this._pdfSetupPromise;
1143
+ }
1144
+
856
1145
  const transformedUrl = transformUrlForDocker(url);
857
1146
  const result = await this._page.goto(transformedUrl, options);
858
1147
  const content = await this._page.content();
@@ -863,6 +1152,21 @@ class SkyrampPlaywrightPage {
863
1152
  } else {
864
1153
  debug(`javascript not detected when visiting ${this._page.url()}`);
865
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
+
866
1170
  return result;
867
1171
  }
868
1172
 
@@ -947,6 +1251,28 @@ class SkyrampPageAssertions {
947
1251
  return await this._playwrightExpectation.toHaveScreenshot(nameOrOptions, options);
948
1252
  }
949
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
+
950
1276
  // Use Playwright's official API to get snapshot path
951
1277
  // playwright.config.js
952
1278
  //export default {
@@ -970,14 +1296,14 @@ class SkyrampPageAssertions {
970
1296
  animations: 'disabled',
971
1297
  caret: 'hide',
972
1298
  scale: 'css',
973
- ...options,
1299
+ ...adjustedOptions,
974
1300
  path: snapshotPath // Always use our computed path
975
1301
  });
976
1302
  debug(`Generated baseline: ${snapshotPath}`);
977
1303
  }
978
1304
 
979
1305
  // Baseline exists (or just created): assert normally
980
- return await this._playwrightExpectation.toHaveScreenshot(nameOrOptions, options);
1306
+ return await this._playwrightExpectation.toHaveScreenshot(nameOrOptions, adjustedOptions);
981
1307
  }
982
1308
  }
983
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;