fetch_util 0.5.2 → 0.6.0

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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +3 -1
  4. data/Rakefile +0 -2
  5. data/lib/fetch_util/assets/extract.js +1 -1
  6. data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +12 -17
  7. data/lib/fetch_util/browser/interaction_helpers/timing_helpers.rb +42 -15
  8. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_commit_details_script.rb +48 -0
  9. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_product_state_script.rb +80 -0
  10. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_helpers_script.rb +53 -0
  11. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_state_script.rb +118 -0
  12. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_stabilization.rb +54 -0
  13. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_state.rb +27 -0
  14. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_product_state.rb +55 -0
  15. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_request_state.rb +100 -0
  16. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_state.rb +59 -0
  17. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_modules.rb +26 -0
  18. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_state.rb +93 -0
  19. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resources.rb +73 -0
  20. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_thread_state.rb +92 -0
  21. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_threads.rb +71 -0
  22. data/lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb +9 -10
  23. data/lib/fetch_util/browser/site_stabilization/facebook_stabilization.rb +56 -0
  24. data/lib/fetch_util/browser/site_stabilization/gerrit_change_product_state_script.rb +97 -0
  25. data/lib/fetch_util/browser/site_stabilization/gerrit_change_request_state_script.rb +75 -0
  26. data/lib/fetch_util/browser/site_stabilization/gerrit_change_stabilization.rb +37 -0
  27. data/lib/fetch_util/browser/site_stabilization/gerrit_change_state.rb +39 -0
  28. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_fetch_state_script.rb +31 -0
  29. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_product_state_script.rb +76 -0
  30. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_request_state_script.rb +111 -0
  31. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_route_state_script.rb +71 -0
  32. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_stabilization.rb +37 -0
  33. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_state.rb +42 -0
  34. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_validation_state_script.rb +49 -0
  35. data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_product_state.rb +88 -0
  36. data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_state.rb +109 -0
  37. data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resources.rb +30 -0
  38. data/lib/fetch_util/browser/site_stabilization/gitea_family_stabilization.rb +60 -0
  39. data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_product_state.rb +74 -0
  40. data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_timeline_state.rb +69 -0
  41. data/lib/fetch_util/browser/site_stabilization/gitea_family_threads.rb +28 -0
  42. data/lib/fetch_util/browser/site_stabilization/gitea_family_visibility_script.rb +28 -0
  43. data/lib/fetch_util/browser/site_stabilization/github_pull_resource_stabilization.rb +31 -0
  44. data/lib/fetch_util/browser/site_stabilization/github_pull_resources.rb +105 -0
  45. data/lib/fetch_util/browser/site_stabilization/github_threads.rb +82 -0
  46. data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state.rb +17 -0
  47. data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state_script.rb +98 -0
  48. data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_visibility_script.rb +43 -0
  49. data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resources.rb +28 -0
  50. data/lib/fetch_util/browser/site_stabilization/gitlab_repo.rb +3 -11
  51. data/lib/fetch_util/browser/site_stabilization/gitlab_stabilization.rb +33 -0
  52. data/lib/fetch_util/browser/site_stabilization/gitlab_threads.rb +92 -0
  53. data/lib/fetch_util/browser/site_stabilization/social_platforms.rb +19 -38
  54. data/lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb +3 -3
  55. data/lib/fetch_util/browser/site_stabilization.rb +89 -0
  56. data/lib/fetch_util/browser/stabilization/anubis.rb +4 -5
  57. data/lib/fetch_util/browser/stabilization/page_flow.rb +71 -27
  58. data/lib/fetch_util/browser/stabilization/spa_hydration.rb +7 -5
  59. data/lib/fetch_util/browser.rb +88 -17
  60. data/lib/fetch_util/cli.rb +69 -23
  61. data/lib/fetch_util/extractor.rb +8 -2
  62. data/lib/fetch_util/fetcher.rb +144 -43
  63. data/lib/fetch_util/http_redirect_client.rb +161 -0
  64. data/lib/fetch_util/parallel_fetcher.rb +125 -28
  65. data/lib/fetch_util/raw_docs_fallback.rb +158 -21
  66. data/lib/fetch_util/regulatory/cache_store.rb +47 -14
  67. data/lib/fetch_util/regulatory/fetch_records.rb +10 -7
  68. data/lib/fetch_util/regulatory/headers.rb +8 -1
  69. data/lib/fetch_util/regulatory/http_client.rb +2 -97
  70. data/lib/fetch_util/regulatory/orchestration.rb +10 -6
  71. data/lib/fetch_util/regulatory/page.rb +8 -11
  72. data/lib/fetch_util/regulatory/robots.rb +2 -10
  73. data/lib/fetch_util/regulatory/source_selection.rb +13 -3
  74. data/lib/fetch_util/regulatory/tdm_page.rb +9 -4
  75. data/lib/fetch_util/regulatory/tdm_policy.rb +16 -9
  76. data/lib/fetch_util/regulatory/tdm_rep.rb +4 -3
  77. data/lib/fetch_util/regulatory/tdm_support.rb +10 -6
  78. data/lib/fetch_util/regulatory.rb +5 -1
  79. data/lib/fetch_util/request_log.rb +24 -3
  80. data/lib/fetch_util/result.rb +37 -89
  81. data/lib/fetch_util/search_transport.rb +102 -35
  82. data/lib/fetch_util/searcher.rb +15 -13
  83. data/lib/fetch_util/version.rb +1 -1
  84. data/lib/fetch_util.rb +4 -1
  85. metadata +66 -2
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FetchUtil
4
+ class Browser
5
+ module SiteStabilization
6
+ module GithubPullResources
7
+ GITHUB_PULL_RESOURCE_STABILIZATION_PROFILE = {
8
+ host: "github.com",
9
+ path_query: ->(uri) { uri.path.match?(%r{\A/[^/]+/[^/]+/pull/\d+/(?:commits|checks|files)/?\z}) },
10
+ strategy: :stabilize_github_pull_resource,
11
+ notes: "Wait for public GitHub pull-request resource lists and selected detail.",
12
+ tests: "spec/fetch_util/browser_stabilization_spec.rb"
13
+ }.freeze
14
+
15
+ private
16
+
17
+ def github_pull_resource_state_script
18
+ <<~'JS'
19
+ (() => {
20
+ const match = location.pathname.match(/\/pull\/\d+\/(commits|checks|files)\/?$/);
21
+ if (!match) return null;
22
+
23
+ const surface = match[1];
24
+ const commitRoot = document.querySelector('[data-testid="commits-list"]');
25
+ const commitRows = document.querySelectorAll('[data-testid="commit-row-item"]');
26
+ const checksRoot = document.querySelector('section.js-selected-check-run, .actions-grid-container, [class*="Checks-module__checksRoot"]');
27
+ const checkLinks = document.querySelectorAll('a[href*="check_run_id="]');
28
+ const requestedCheckId = new URLSearchParams(location.search).get('check_run_id') || '';
29
+ const selectedCheck = /^\d+$/.test(requestedCheckId) ? document.getElementById('check_run_' + requestedCheckId) : null;
30
+ const fileHeaders = document.querySelectorAll('.file.js-file .file-header[data-path][data-anchor]');
31
+ const selectedFile = location.hash && document.querySelector('.file-header[data-anchor="' + CSS.escape(location.hash.slice(1)) + '"]');
32
+ const selectedFileRoot = selectedFile && selectedFile.closest('.file.js-file');
33
+ const bodySelector = '.js-file-content, .blob-wrapper, table.diff-table, table, [data-testid="diff-lines"], [data-testid="diff-file-content"], .rendered-diff';
34
+ const deferredSelector = '.js-diff-load-container include-fragment[src], .js-diff-load-container[data-fragment-url], include-fragment[src*="/pull/"][src*="/files"]';
35
+ const nodeVisible = (node) => {
36
+ if (!node) return false;
37
+ const nodeStyle = getComputedStyle(node);
38
+ if (nodeStyle.visibility === 'hidden' || nodeStyle.visibility === 'collapse') return false;
39
+ let current = node;
40
+ while (current && current.nodeType === 1) {
41
+ if (current.hidden || getComputedStyle(current).display === 'none') return false;
42
+ current = current.parentElement;
43
+ }
44
+ return true;
45
+ };
46
+ const materialTextSize = (node) => {
47
+ if (!node) return 0;
48
+ return [node, ...node.querySelectorAll('*')].reduce((total, candidate) => {
49
+ if (!nodeVisible(candidate)) return total;
50
+ const directText = Array.from(candidate.childNodes)
51
+ .filter((child) => child.nodeType === Node.TEXT_NODE)
52
+ .map((child) => child.textContent || '')
53
+ .join('')
54
+ .trim();
55
+ return total + directText.length;
56
+ }, 0);
57
+ };
58
+ const materialNode = (node) => {
59
+ if (materialTextSize(node) > 0) return true;
60
+ return [node, ...node.querySelectorAll('*')].some((candidate) => {
61
+ return nodeVisible(candidate) && candidate.matches('img[src], canvas, video, audio');
62
+ });
63
+ };
64
+
65
+ let loaded = 0;
66
+ let selectedSize = 0;
67
+ let selectedRequested = false;
68
+ let selectedLoaded = false;
69
+ let selectedDeferred = false;
70
+ let explicitEmpty = false;
71
+ if (surface === 'commits') loaded = commitRows.length;
72
+ if (surface === 'checks') {
73
+ loaded = checkLinks.length;
74
+ selectedRequested = !!requestedCheckId;
75
+ selectedSize = nodeVisible(selectedCheck) ? (selectedCheck.textContent || '').trim().length : 0;
76
+ explicitEmpty = /no checks (?:have been|were) run/i.test(checksRoot && checksRoot.textContent || '');
77
+ }
78
+ if (surface === 'files') {
79
+ loaded = fileHeaders.length;
80
+ selectedRequested = !!location.hash;
81
+ const selectedBody = selectedFileRoot && Array.from(selectedFileRoot.querySelectorAll(bodySelector)).find(materialNode);
82
+ selectedLoaded = !!selectedBody;
83
+ selectedDeferred = !!(selectedFileRoot && Array.from(selectedFileRoot.querySelectorAll(deferredSelector)).some((node) =>
84
+ nodeVisible(node) && (node.getAttribute('src') || node.getAttribute('data-fragment-url'))
85
+ ));
86
+ selectedSize = selectedLoaded ? materialTextSize(selectedBody) : 0;
87
+ explicitEmpty = /(?:no files (?:were )?changed|there are no files)/i.test((document.querySelector('.blankslate, main') || {}).textContent || '');
88
+ }
89
+
90
+ const rootReady = surface === 'commits' ? !!commitRoot : (surface === 'checks' ? !!checksRoot : (loaded > 0 || explicitEmpty));
91
+ const selectedReady = explicitEmpty || !selectedRequested || selectedLoaded || selectedDeferred;
92
+ const ready = rootReady && (loaded > 0 || explicitEmpty) && selectedReady;
93
+ return {
94
+ ready,
95
+ selectedLoaded,
96
+ selectedDeferred,
97
+ signature: [surface, loaded, selectedLoaded, selectedDeferred, selectedSize, location.search, location.hash].join(':')
98
+ };
99
+ })()
100
+ JS
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FetchUtil
4
+ class Browser
5
+ module SiteStabilization
6
+ module GithubThreads
7
+ GITHUB_THREAD_STABILIZATION_PROFILE = {
8
+ host: "github.com",
9
+ path_query: ->(uri) { uri.path.match?(%r{\A/[^/]+/[^/]+/(?:issues|pull|discussions)/\d+/?\z}) },
10
+ strategy: :stabilize_github_thread,
11
+ notes: "Wait for a public GitHub thread body and its materialized timeline.",
12
+ tests: "spec/fetch_util/browser_stabilization_spec.rb"
13
+ }.freeze
14
+
15
+ private
16
+
17
+ def stabilize_github_thread(page, deadline: stabilization_deadline)
18
+ last_signature = nil
19
+ stable_observations = 0
20
+ ready = retry_until_timeout(capped_timeout(6.0, deadline: deadline), interval: 0.1, deadline: deadline) do
21
+ state = safe_evaluate(page, <<~JS, default: nil)
22
+ (() => {
23
+ const root = document.querySelector(
24
+ '[data-testid="issue-viewer-container"], [data-testid="issue-viewer-issue-container"], #discussion_bucket, .js-discussion, .discussion-timeline'
25
+ );
26
+ if (!root) return null;
27
+
28
+ const modernBody = root.querySelector('[data-testid="issue-body"]');
29
+ if (!modernBody) {
30
+ return { ready: !!root.querySelector('.comment-body, .js-comment-body, [data-testid="comment-body"], [itemprop="text"]'), complete: true, signature: 'legacy' };
31
+ }
32
+
33
+ const author = modernBody.querySelector('[data-testid="issue-body-header-author"]');
34
+ const rows = root.querySelectorAll('[data-testid^="timeline-row-border-"]');
35
+ const commentRows = Array.from(rows).filter((row) => row.querySelector('a[href*="#issuecomment-"], a[href*="#discussioncomment-"]'));
36
+ const count = root.querySelector('[data-testid="issue-comment-count"], [data-testid="discussion-comment-count"]');
37
+ const countText = (count && count.textContent || '').trim();
38
+ const explicitZero = /^0\s+(?:comments?|replies)$/i.test(countText);
39
+ const continuationControls = Array.from(root.querySelectorAll(
40
+ 'a[rel~="next"][href*="timeline_page="], a[data-testid*="timeline-load-more"][href*="timeline_page="], ' +
41
+ '[data-testid*="timeline-load-more"] a[href*="timeline_page="], ' +
42
+ '[data-testid*="timeline-load-more"] button, button[data-testid*="timeline-load-more"]'
43
+ ));
44
+ const usableContinuation = continuationControls.some((control) => {
45
+ if (control.disabled || control.getAttribute('aria-disabled') === 'true') return false;
46
+
47
+ let current = control;
48
+ while (current && root.contains(current)) {
49
+ if (current.hidden || current.getAttribute('aria-hidden') === 'true') return false;
50
+ const style = getComputedStyle(current);
51
+ if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') return false;
52
+ current = current.parentElement;
53
+ }
54
+ return true;
55
+ });
56
+ return {
57
+ ready: !!author && (explicitZero || rows.length > 0),
58
+ complete: false,
59
+ signature: [rows.length, commentRows.length, countText, usableContinuation].join(':')
60
+ };
61
+ })()
62
+ JS
63
+ next false unless state.is_a?(Hash) && state["ready"]
64
+
65
+ if state["complete"]
66
+ true
67
+ elsif state["signature"] == last_signature
68
+ stable_observations += 1
69
+ stable_observations >= 3
70
+ else
71
+ last_signature = state["signature"]
72
+ stable_observations = 1
73
+ false
74
+ end
75
+ end
76
+
77
+ settle_after_stabilization(0.5, deadline: deadline) if ready
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FetchUtil
4
+ class Browser
5
+ module SiteStabilization
6
+ module GitlabMergeRequestResourceState
7
+ include GitlabMergeRequestResourceStateScript
8
+
9
+ private
10
+
11
+ def gitlab_merge_request_resource_state(page)
12
+ safe_evaluate(page, gitlab_merge_request_resource_state_script, default: nil)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FetchUtil
4
+ class Browser
5
+ module SiteStabilization
6
+ module GitlabMergeRequestResourceStateScript
7
+ include GitlabMergeRequestResourceVisibilityScript
8
+
9
+ private
10
+
11
+ def gitlab_merge_request_resource_state_script
12
+ <<~JS
13
+ (() => {
14
+ const match = location.pathname.match(/\/-\/merge_requests\/\d+\/(commits|pipelines|reports(?:\/[^/]+)?|diffs)\/?$/);
15
+ if (!match) return null;
16
+ const surface = match[1].split('/')[0];
17
+ const site = document.querySelector('meta[property="og:site_name"]');
18
+ const branded = document.documentElement.classList.contains('gl-system') &&
19
+ /^GitLab$/i.test((site && site.content || '').trim());
20
+ const meta = !!document.querySelector('meta[name="gitlab-meta"], meta[name^="gitlab-"]');
21
+ const gon = window.gon || {};
22
+ const hasRelativeRoot = Object.prototype.hasOwnProperty.call(gon, 'relative_url_root');
23
+ const runtimeTarget = gon.gitlab_url || (hasRelativeRoot ? (gon.relative_url_root || location.origin) : '');
24
+ let runtime = !!gon.api_version && !!runtimeTarget;
25
+ if (runtime) {
26
+ try { runtime = new URL(runtimeTarget, location.href).origin === location.origin; } catch (_error) { runtime = false; }
27
+ }
28
+ const asset = Array.from(document.querySelectorAll('script[src], link[href]')).some((node) => {
29
+ const value = node.getAttribute('src') || node.getAttribute('href') || '';
30
+ if (!/\/assets\/(?:webpack|application|gitlab)/i.test(value)) return false;
31
+ try { return new URL(value, location.href).origin === location.origin; } catch (_error) { return false; }
32
+ });
33
+ const product = (runtime && (branded || meta || asset)) || (branded && (meta || asset));
34
+ if (!product) return { product: false, ready: false, signature: '' };
35
+
36
+ const root = document.querySelector('main') ||
37
+ document.querySelector('.content-wrapper') ||
38
+ document.querySelector('.merge-request') ||
39
+ document.querySelector('[data-page]') ||
40
+ document.body;
41
+ #{gitlab_merge_request_resource_visibility_script}
42
+ const selectors = {
43
+ commits: '[data-testid="commit-content"], [data-testid="commit-row"], .commit-row, li.commit',
44
+ pipelines: '[data-testid="pipeline-table-row"], .pipeline-row, .ci-table tbody tr, table.pipelines tbody tr',
45
+ reports: '[data-testid*="report"], .report-block, .mr-widget-reports > *, .report-section',
46
+ diffs: 'diff-file[data-testid="rd-diff-file"], .diff-file, .file-holder, [data-testid="diff-file"]'
47
+ };
48
+ const candidates = Array.from(root.querySelectorAll(selectors[surface])).filter(material);
49
+ const rows = candidates.filter((row, index) => !candidates.some(
50
+ (other, otherIndex) => otherIndex < index && other.contains(row)
51
+ ));
52
+ const empty = Array.from(root.querySelectorAll(
53
+ '[data-testid="empty-state"], .gl-empty-state, .empty-state, .nothing-here-block, .blank-state'
54
+ )).find((node) => material(node) && materialText(node));
55
+ const loading = Array.from(document.querySelectorAll('[aria-busy="true"], .gl-spinner, .loading, .is-loading'))
56
+ .some(material);
57
+ let selectedId = '';
58
+ if (surface === 'diffs') {
59
+ try { selectedId = decodeURIComponent((location.hash || '').replace(/^#/, '')); } catch (_error) { selectedId = ''; }
60
+ }
61
+ const explicit = (row) => row.id || row.getAttribute('data-diff-id') ||
62
+ row.getAttribute('data-file-id') || row.getAttribute('data-file-path') || '';
63
+ const reserved = new Set(rows.map(explicit).filter(Boolean));
64
+ const assigned = new Set();
65
+ const rowIds = rows.map((row, index) => {
66
+ const value = explicit(row);
67
+ if (value && !assigned.has(value)) {
68
+ assigned.add(value);
69
+ return value;
70
+ }
71
+ let identity = value
72
+ ? `${value}-fetch-util-${index + 1}`
73
+ : `fetch-util-diff-${index + 1}`;
74
+ while (reserved.has(identity) || assigned.has(identity)) identity += '-';
75
+ assigned.add(identity);
76
+ return identity;
77
+ });
78
+ const selectedIndex = selectedId ? rowIds.indexOf(selectedId) : -1;
79
+ const selected = !selectedId || selectedIndex >= 0 || !!empty;
80
+ const bodySelector = [
81
+ '[data-testid="rd-diff-file-body"]', '[data-testid="diff-file-body"]', '.diff-content',
82
+ '.file-content', '.diff-table', 'table.diff-table', '.diff-line', '.line_content'
83
+ ].join(', ');
84
+ const selectedBody = !selectedId || !!empty || (selectedIndex >= 0 &&
85
+ Array.from(rows[selectedIndex].querySelectorAll(bodySelector)).some((node) => materialText(node)));
86
+ const textSize = rows.reduce((sum, row) => sum + materialText(row).length, 0);
87
+ return {
88
+ product: true,
89
+ ready: !loading && selected && selectedBody && (rows.length > 0 || !!empty),
90
+ signature: [surface, rows.length, !!empty, selected, selectedBody, textSize].join(':')
91
+ };
92
+ })()
93
+ JS
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FetchUtil
4
+ class Browser
5
+ module SiteStabilization
6
+ module GitlabMergeRequestResourceVisibilityScript
7
+ private
8
+
9
+ def gitlab_merge_request_resource_visibility_script
10
+ <<~JS
11
+ const subtreeHidden = (node) => {
12
+ let current = node;
13
+ while (current && current !== document) {
14
+ const style = getComputedStyle(current);
15
+ if (current.hidden || style.display === 'none') return true;
16
+ current = current.parentElement;
17
+ }
18
+ return false;
19
+ };
20
+ const materialText = (node) => {
21
+ if (!node || subtreeHidden(node)) return '';
22
+ const collect = (current) => {
23
+ if (current.nodeType === Node.TEXT_NODE) {
24
+ const style = getComputedStyle(current.parentElement);
25
+ return /^(?:hidden|collapse)$/.test(style.visibility) ? '' : current.nodeValue;
26
+ }
27
+ if (current.nodeType !== Node.ELEMENT_NODE) return '';
28
+ const style = getComputedStyle(current);
29
+ if (current.hidden || style.display === 'none') return '';
30
+ return Array.from(current.childNodes).map(collect).join('');
31
+ };
32
+ return collect(node).trim();
33
+ };
34
+ const material = (node) => {
35
+ if (subtreeHidden(node)) return false;
36
+ return !/^(?:hidden|collapse)$/.test(getComputedStyle(node).visibility) || !!materialText(node);
37
+ };
38
+ JS
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FetchUtil
4
+ class Browser
5
+ module SiteStabilization
6
+ module GitlabMergeRequestResources
7
+ include GitlabMergeRequestResourceState
8
+
9
+ GITLAB_MERGE_REQUEST_RESOURCE_STABILIZATION_PROFILE = {
10
+ host: true,
11
+ path_query: lambda { |uri|
12
+ uri.path.match?(%r{/-/merge_requests/\d+/(?:commits|pipelines|reports(?:/[^/]+)?|diffs)/?\z})
13
+ },
14
+ strategy: :stabilize_gitlab_merge_request_resource,
15
+ fallthrough: true,
16
+ notes: "Wait for host-agnostic GitLab merge-request resource pages.",
17
+ tests: "spec/fetch_util/browser_stabilization_spec.rb"
18
+ }.freeze
19
+
20
+ private
21
+
22
+ def stabilize_gitlab_merge_request_resource(page, deadline: stabilization_deadline)
23
+ stabilize_gitlab_state(deadline: deadline) { gitlab_merge_request_resource_state(page) }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -6,16 +6,8 @@ module FetchUtil
6
6
  module GitlabRepo
7
7
  private
8
8
 
9
- def gitlab_repo_url?(url)
10
- uri = URI.parse(url)
11
- host = uri.host.to_s.downcase
12
- (host == "gitlab.com" || host.end_with?(".gitlab.com")) && uri.path.split("/").reject(&:empty?).length == 2
13
- rescue URI::InvalidURIError
14
- false
15
- end
16
-
17
- def stabilize_gitlab_repo(page)
18
- retry_until_timeout(capped_timeout(8.0), interval: 0.2) do
9
+ def stabilize_gitlab_repo(page, deadline: stabilization_deadline)
10
+ retry_until_timeout(capped_timeout(8.0, deadline: deadline), interval: 0.2, deadline: deadline) do
19
11
  safe_evaluate(page, <<~JS, default: 0).to_i >= 300
20
12
  (() => {
21
13
  const readme = document.querySelector('[data-testid="blob-viewer-content"] .blob-viewer[data-path="README.md"] .file-content.md, .blob-viewer[data-path="README.md"] .file-content.md');
@@ -24,7 +16,7 @@ module FetchUtil
24
16
  JS
25
17
  end
26
18
 
27
- settle_after_stabilization(0.25)
19
+ settle_after_stabilization(0.25, deadline: deadline)
28
20
  end
29
21
  end
30
22
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FetchUtil
4
+ class Browser
5
+ module SiteStabilization
6
+ module GitlabStabilization
7
+ private
8
+
9
+ def stabilize_gitlab_state(deadline: stabilization_deadline)
10
+ last_signature = nil
11
+ stable_observations = 0
12
+ ready = retry_until_timeout(capped_timeout(8.0, deadline: deadline), interval: 0.1, deadline: deadline) do
13
+ state = yield
14
+ return false if state.is_a?(Hash) && state["product"] == false
15
+ next false unless state.is_a?(Hash) && state["product"] && state["ready"]
16
+
17
+ if state["signature"] == last_signature
18
+ stable_observations += 1
19
+ stable_observations >= 3
20
+ else
21
+ last_signature = state["signature"]
22
+ stable_observations = 1
23
+ false
24
+ end
25
+ end
26
+
27
+ settle_after_stabilization(0.5, deadline: deadline) if ready
28
+ !!ready
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FetchUtil
4
+ class Browser
5
+ module SiteStabilization
6
+ module GitlabThreads
7
+ GITLAB_THREAD_STABILIZATION_PROFILE = {
8
+ host: true,
9
+ path_query: lambda { |uri|
10
+ uri.path.match?(%r{/[^/]+/-/(?:issues|work_items|merge_requests)/\d+/?\z})
11
+ },
12
+ strategy: :stabilize_gitlab_thread,
13
+ fallthrough: true,
14
+ notes: "Wait for host-agnostic GitLab issue and merge-request conversations.",
15
+ tests: "spec/fetch_util/browser_stabilization_spec.rb"
16
+ }.freeze
17
+
18
+ private
19
+
20
+ def stabilize_gitlab_thread(page, deadline: stabilization_deadline)
21
+ stabilize_gitlab_state(deadline: deadline) do
22
+ safe_evaluate(page, <<~JS, default: nil)
23
+ (() => {
24
+ const route = location.pathname.match(/\/-\/(issues|work_items|merge_requests)\/\d+\/?$/);
25
+ if (!route) return null;
26
+
27
+ const site = document.querySelector('meta[property="og:site_name"]');
28
+ const branded = document.documentElement.classList.contains('gl-system') &&
29
+ /^GitLab$/i.test((site && site.content || '').trim());
30
+ const meta = !!document.querySelector('meta[name="gitlab-meta"], meta[name^="gitlab-"]');
31
+ const gon = window.gon || {};
32
+ const hasRelativeRoot = Object.prototype.hasOwnProperty.call(gon, 'relative_url_root');
33
+ const runtimeTarget = gon.gitlab_url || (hasRelativeRoot ? (gon.relative_url_root || location.origin) : '');
34
+ let runtime = !!gon.api_version && !!runtimeTarget;
35
+ if (runtime) {
36
+ try { runtime = new URL(runtimeTarget, location.href).origin === location.origin; } catch (_error) { runtime = false; }
37
+ }
38
+ const asset = Array.from(document.querySelectorAll('script[src], link[href]')).some((node) => {
39
+ const value = node.getAttribute('src') || node.getAttribute('href') || '';
40
+ if (!/\/assets\/(?:webpack|application|gitlab)/i.test(value)) return false;
41
+ try { return new URL(value, location.href).origin === location.origin; } catch (_error) { return false; }
42
+ });
43
+ const product = (runtime && (branded || meta || asset)) || (branded && (meta || asset));
44
+ if (!product) return { product: false, ready: false, signature: '' };
45
+
46
+ const root = document.querySelector(
47
+ '.work-item-page, [data-testid="work-item-view"], .work-item-notes, .issuable-details, .merge-request, .issue-details'
48
+ );
49
+ if (!root) return { product: true, ready: false, signature: '' };
50
+
51
+ const title = document.querySelector('[data-testid="work-item-title"], [data-testid="issuable-title"], .issue-title, .merge-request .title');
52
+ const description = document.querySelector('[data-testid="work-item-description"], [data-testid="description-content"], .detail-page-description .md, .issuable-description .md');
53
+ const rows = document.querySelectorAll('.js-timeline-entry.timeline-entry, .timeline-entry');
54
+ const bodies = document.querySelectorAll('.note-body, .note-text, [data-testid="note-body"]');
55
+ const explicitZero = /\b0\s+(?:comments?|notes?)\b/i.test((root.textContent || ''));
56
+ const loadControls = Array.from(root.querySelectorAll(
57
+ 'button[data-testid*="load-more"], a[data-testid*="load-more"], [data-testid*="load-more"] button, [data-testid*="load-more"] a, .js-load-more button, .js-load-more a'
58
+ )).filter((node) => {
59
+ let current = node;
60
+ while (current && current !== root.parentElement) {
61
+ const style = getComputedStyle(current);
62
+ if (current.hidden || style.display === 'none') return false;
63
+ current = current.parentElement;
64
+ }
65
+ return getComputedStyle(node).visibility !== 'hidden';
66
+ });
67
+ const loading = loadControls.some((node) =>
68
+ node.getAttribute('aria-busy') === 'true' || node.getAttribute('data-loading') === 'true' ||
69
+ /(?:^|\s)(?:is-)?loading(?:\s|$)/.test(node.className || '')
70
+ );
71
+ const continuation = !loading && loadControls.find((node) =>
72
+ !node.disabled && node.getAttribute('aria-disabled') !== 'true'
73
+ );
74
+ if (continuation) {
75
+ continuation.click();
76
+ return { product: true, ready: false, signature: [rows.length, bodies.length, true, 'loading'].join(':') };
77
+ }
78
+ const textSize = Array.from(rows).reduce((sum, row) => sum + (row.textContent || '').trim().length, 0) +
79
+ (description && description.textContent || '').trim().length;
80
+ return {
81
+ product: true,
82
+ ready: !!title && !!description && !loading && (rows.length > 0 || explicitZero),
83
+ signature: [rows.length, bodies.length, loading, textSize].join(':')
84
+ };
85
+ })()
86
+ JS
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -15,13 +15,27 @@ module FetchUtil
15
15
 
16
16
  private
17
17
 
18
- def stabilize_instagram(page)
19
- wait_for_idle_or_content(page) if @wait_for_idle
18
+ def stabilize_instagram(page, deadline: stabilization_deadline)
19
+ wait_for_idle_or_content(page, deadline: deadline) if @wait_for_idle
20
+ return false unless stabilization_time_remaining?(deadline)
21
+
20
22
  accept_instagram_cookie_dialog(page) || accept_cookie_consent(page)
21
- social_login_phase_pause
23
+ return false unless stabilization_time_remaining?(deadline)
24
+
25
+ social_login_phase_pause(deadline: deadline)
26
+ return false unless stabilization_time_remaining?(deadline)
27
+
22
28
  accept_instagram_cookie_dialog(page) || accept_cookie_consent(page)
23
- retry_until_timeout(capped_timeout(5.0)) { dismiss_instagram_login_modal(page) }
24
- social_login_phase_pause
29
+ return false unless stabilization_time_remaining?(deadline)
30
+
31
+ retry_until_timeout(capped_timeout(5.0, deadline: deadline), deadline: deadline) do
32
+ dismiss_instagram_login_modal(page)
33
+ end
34
+ return false unless stabilization_time_remaining?(deadline)
35
+
36
+ social_login_phase_pause(deadline: deadline)
37
+ return false unless stabilization_time_remaining?(deadline)
38
+
25
39
  dismiss_instagram_login_modal(page)
26
40
  end
27
41
 
@@ -72,39 +86,6 @@ module FetchUtil
72
86
  allow_empty_close_label: true
73
87
  )
74
88
  end
75
-
76
- def stabilize_facebook(page)
77
- wait_for_idle_or_content(page) if @wait_for_idle
78
- social_login_phase_pause
79
- dismiss_facebook_cookie_dialog(page)
80
- social_login_phase_pause
81
- retry_until_timeout(capped_timeout(5.0)) { dismiss_facebook_login_dialog(page) }
82
- social_login_phase_pause
83
- end
84
-
85
- def dismiss_facebook_cookie_dialog(page)
86
- config = consent_config(
87
- accept_labels: ["decline optional cookies", "optionale cookies ablehnen", "refuser les cookies optionnels",
88
- "rechazar cookies opcionales", "rifiuta i cookie opzionali"],
89
- fallback_labels: ["allow all cookies", "alle cookies erlauben", "autoriser tous les cookies", "permitir todas las cookies", "consenti tutti i cookie"]
90
- )
91
-
92
- click_visible_button_by_text(
93
- page,
94
- consent_accept_labels(config),
95
- consent_fallback_labels(config),
96
- selectors: consent_button_selectors(config)
97
- )
98
- end
99
-
100
- def dismiss_facebook_login_dialog(page)
101
- dismiss_overlay_dialog(
102
- page,
103
- close_selectors: ['[aria-label="Close"]', '[aria-label="close"]'],
104
- dialog_selectors: ['[role="dialog"]', '[aria-modal="true"]'],
105
- dialog_pattern: "log in|sign up|create (?:new )?account|see more from"
106
- )
107
- end
108
89
  end
109
90
  end
110
91
  end
@@ -16,10 +16,10 @@ module FetchUtil
16
16
 
17
17
  private
18
18
 
19
- def stabilize_lodging_detail(page)
19
+ def stabilize_lodging_detail(page, deadline: stabilization_deadline)
20
20
  accepted_cookies = dismiss_cookie_overlays(page)
21
21
 
22
- retry_until_timeout(capped_timeout(8.0), interval: 0.25) do
22
+ retry_until_timeout(capped_timeout(8.0, deadline: deadline), interval: 0.25, deadline: deadline) do
23
23
  safe_evaluate(page, <<~JS, default: false)
24
24
  (() => {
25
25
  const bodyText = document.body ? (document.body.innerText || '') : '';
@@ -31,7 +31,7 @@ module FetchUtil
31
31
  JS
32
32
  end
33
33
 
34
- settle_after_stabilization(0.25) if accepted_cookies
34
+ settle_after_stabilization(0.25, deadline: deadline) if accepted_cookies
35
35
  end
36
36
  end
37
37
  end