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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +3 -1
- data/Rakefile +0 -2
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +12 -17
- data/lib/fetch_util/browser/interaction_helpers/timing_helpers.rb +42 -15
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_commit_details_script.rb +48 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_product_state_script.rb +80 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_helpers_script.rb +53 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_state_script.rb +118 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_stabilization.rb +54 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_state.rb +27 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_product_state.rb +55 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_request_state.rb +100 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_state.rb +59 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_modules.rb +26 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_state.rb +93 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resources.rb +73 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_thread_state.rb +92 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_threads.rb +71 -0
- data/lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb +9 -10
- data/lib/fetch_util/browser/site_stabilization/facebook_stabilization.rb +56 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_product_state_script.rb +97 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_request_state_script.rb +75 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_stabilization.rb +37 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_state.rb +39 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_fetch_state_script.rb +31 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_product_state_script.rb +76 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_request_state_script.rb +111 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_route_state_script.rb +71 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_stabilization.rb +37 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_state.rb +42 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_validation_state_script.rb +49 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_product_state.rb +88 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_state.rb +109 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resources.rb +30 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_stabilization.rb +60 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_product_state.rb +74 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_timeline_state.rb +69 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_threads.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_visibility_script.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/github_pull_resource_stabilization.rb +31 -0
- data/lib/fetch_util/browser/site_stabilization/github_pull_resources.rb +105 -0
- data/lib/fetch_util/browser/site_stabilization/github_threads.rb +82 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state.rb +17 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state_script.rb +98 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_visibility_script.rb +43 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resources.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_repo.rb +3 -11
- data/lib/fetch_util/browser/site_stabilization/gitlab_stabilization.rb +33 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_threads.rb +92 -0
- data/lib/fetch_util/browser/site_stabilization/social_platforms.rb +19 -38
- data/lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb +3 -3
- data/lib/fetch_util/browser/site_stabilization.rb +89 -0
- data/lib/fetch_util/browser/stabilization/anubis.rb +4 -5
- data/lib/fetch_util/browser/stabilization/page_flow.rb +71 -27
- data/lib/fetch_util/browser/stabilization/spa_hydration.rb +7 -5
- data/lib/fetch_util/browser.rb +88 -17
- data/lib/fetch_util/cli.rb +69 -23
- data/lib/fetch_util/extractor.rb +8 -2
- data/lib/fetch_util/fetcher.rb +144 -43
- data/lib/fetch_util/http_redirect_client.rb +161 -0
- data/lib/fetch_util/parallel_fetcher.rb +125 -28
- data/lib/fetch_util/raw_docs_fallback.rb +158 -21
- data/lib/fetch_util/regulatory/cache_store.rb +47 -14
- data/lib/fetch_util/regulatory/fetch_records.rb +10 -7
- data/lib/fetch_util/regulatory/headers.rb +8 -1
- data/lib/fetch_util/regulatory/http_client.rb +2 -97
- data/lib/fetch_util/regulatory/orchestration.rb +10 -6
- data/lib/fetch_util/regulatory/page.rb +8 -11
- data/lib/fetch_util/regulatory/robots.rb +2 -10
- data/lib/fetch_util/regulatory/source_selection.rb +13 -3
- data/lib/fetch_util/regulatory/tdm_page.rb +9 -4
- data/lib/fetch_util/regulatory/tdm_policy.rb +16 -9
- data/lib/fetch_util/regulatory/tdm_rep.rb +4 -3
- data/lib/fetch_util/regulatory/tdm_support.rb +10 -6
- data/lib/fetch_util/regulatory.rb +5 -1
- data/lib/fetch_util/request_log.rb +24 -3
- data/lib/fetch_util/result.rb +37 -89
- data/lib/fetch_util/search_transport.rb +102 -35
- data/lib/fetch_util/searcher.rb +15 -13
- data/lib/fetch_util/version.rb +1 -1
- data/lib/fetch_util.rb +4 -1
- 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
|
data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_visibility_script.rb
ADDED
|
@@ -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
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|