| import type { Page, Frame } from 'playwright'; |
|
|
| |
| |
| |
|
|
| export const SELECTORS = { |
| |
| login: { |
| emailInput: '#email', |
| passwordInput: '#password', |
| submitButton: 'input.submit[type="submit"]', |
| }, |
|
|
| |
| eula: { |
| agreementModalSaveBtn: '#eulaAgreementModalSaveBtn', |
| iAgreeButton: 'button:has-text("I Agree")', |
| acceptButton: 'button:has-text("Accept")', |
| tdlIAgreeButton: 'tdl-button:has-text("I Agree")', |
| tdlAcceptButton: 'tdl-button:has-text("Accept")', |
| iAgreeContinueLink: 'a:has-text("I Agree -- Continue")', |
| }, |
|
|
| |
| class: { |
| classNameLink: 'td.class_name a, a.ib-class-link, a', |
| }, |
|
|
| |
| assignment: { |
| assignmentRow: 'tr.assignment-row', |
| openButton: 'a.btn-open, a.btn-primary, button:has-text("Open"), a:has-text("Open")', |
| allOpenButtons: |
| 'tr.assignment-row a.btn-open, a.btn-open, a.btn-primary, button:has-text("Open"), a:has-text("Open")', |
| }, |
|
|
| |
| upload: { |
| fileInput: 'input[type="file"]', |
| browseButton: |
| |
| |
| 'tdl-button[part="upload-form-container-button"], tii-ing-upload-button tdl-button, button:has-text("Browse Files"), button:has-text("Browse"), tdl-button:has-text("Browse")', |
| uploadFromDevice: |
| 'tdl-button[part="upload-form-container-menu-browse-button"], .actions-menu tdl-button[part="upload-form-container-menu-browse-button"], button:has-text("Your device"), tdl-button:has-text("Your device"), button:has-text("Upload from this device"), tdl-button:has-text("Upload from this device")', |
| submitButton: |
| |
| |
| 'tdl-button[part="submit-button"], tdl-button:has-text("Upload and Preview"), tdl-button.accept-button, tii-grn-button, button:has-text("Upload"), tdl-button:has-text("Upload"), button:has-text("Confirm"), tdl-button:has-text("Confirm"), button:has-text("Submit"), tdl-button:has-text("Submit")', |
| |
| uploadStepContainer: 'div.upload-step, tii-ing-dropzone', |
| }, |
|
|
| |
| resubmit: { |
| |
| |
| resubmitButton: |
| 'tii-grn-button:has-text("Resubmit"), button:has-text("Resubmit"), tdl-button:has-text("Resubmit"), [aria-label*="Resubmit"]', |
| |
| submissionActionPanel: 'tii-workflow-student-summary-panel-new, div.submission-action', |
| |
| resubmissionConfirmModal: 'dialog#tii-resubmission-last-submission-modal', |
| |
| resubmissionConfirmAccept: |
| 'dialog#tii-resubmission-last-submission-modal [slot="accept-button"] tdl-button, dialog#tii-resubmission-last-submission-modal .accept-button, dialog#tii-resubmission-last-submission-modal tdl-button.accept-button', |
| }, |
|
|
| |
| similarity: { |
| viewSubmissionButton: |
| 'button.link-button[aria-label*="View submission"], button.link-button, a[aria-label*="View submission"], a.view-mark, .similarity-button', |
| similarityDisplay: 'tii-mpdd-similarity-display', |
| similarityBadge: 'span[part="tii-grn-badge-label"]', |
| similaritySpan: 'tii-mpdd-similarity-display span:has-text("Similarity:")', |
| bodyContainer: '.body-container', |
| linkButtons: 'button.link-button', |
| }, |
|
|
| |
| viewer: { |
| titleButton: |
| 'button.link-button[aria-label*="View submission"], button.link-button, a[aria-label*="View submission"], a.view-mark, .similarity-button, [part="tii-grn-badge-label"]', |
| }, |
|
|
| |
| filters: { |
| similarityTab: |
| [ |
| 'tii-sws-tab-button#tab-similarity', |
| '#tab-similarity', |
| 'tii-sws-tab-button[aria-controls="tii-sws-main"]:has-text("Similarity")', |
| 'tii-sws-tab-button:has-text("Similarity")', |
| 'button.tab-button[data-px="SimilarityTabClicked"]', |
| '[data-px="SimilarityTabClicked"]', |
| '[with-data-px="SimilarityTabClicked"]', |
| '[withdatapx="SimilarityTabClicked"]', |
| '[data-px*="SimilarityTab"]', |
| '[with-data-px*="SimilarityTab"]', |
| '[withdatapx*="SimilarityTab"]', |
| '[role="tab"]:has-text("Similarity")', |
| 'button:has-text("Similarity")', |
| 'tdl-button:has-text("Similarity")', |
| 'tdl-labeled-button:has-text("Similarity")', |
| '[aria-label*="Similarity"]', |
| ].join(', '), |
| filterButton: |
| [ |
| '.tii-SimilarityReportPanel[aria-hidden="false"] .tii-SimilarityReportPanelHeader__SettingsButton', |
| '.tii-SimilarityReportPanel[aria-hidden="false"] tdl-button[with-data-px="SettingsClicked"]', |
| '.tii-filters-and-exclusions-container tdl-button[with-data-px="SettingsClicked"]', |
| 'tdl-button[with-data-px="SettingsClicked"]', |
| 'tdl-button[withdatapx="SettingsClicked"]', |
| '[with-data-px="SettingsClicked"]', |
| '[withdatapx="SettingsClicked"]', |
| '[data-px="SettingsClicked"]', |
| '[data-px*="Settings"]', |
| '[with-data-px*="Settings"]', |
| '[withdatapx*="Settings"]', |
| '.tii-SimilarityReportPanelHeader__SettingsButton', |
| 'tdl-button:has-text("Filters")', |
| 'tdl-labeled-button:has-text("Filters")', |
| 'tii-grn-button:has-text("Filters")', |
| 'button:has-text("Filters")', |
| '[role="button"]:has-text("Filters")', |
| '[aria-label*="Filter"]', |
| '[title*="Filter"]', |
| '[part*="filter"]', |
| ].join(', '), |
| excludeBibliography: '#excludeBibliography, [with-id="excludeBibliography"]', |
| excludeQuotes: '#excludeQuotes, [with-id="excludeQuotes"]', |
| excludeCitations: '#excludeCitations, [with-id="excludeCitations"]', |
| excludeSmallMatches: |
| 'tdl-checkbox[with-px-label="ExcludeSmallMatches"], tdl-checkbox:has-text("Exclude small matches")', |
| smallMatchesInput: |
| 'tdl-number-input.small-matches-input, tdl-number-input[label="Set match exclusion threshold"]', |
| applyFilters: |
| [ |
| 'tdl-button[with-data-px="ApplyFiltersClicked"]', |
| 'tdl-button[withdatapx="ApplyFiltersClicked"]', |
| '[with-data-px="ApplyFiltersClicked"]', |
| '[withdatapx="ApplyFiltersClicked"]', |
| '[data-px="ApplyFiltersClicked"]', |
| 'tdl-button[slot="accept-button"]:has-text("Apply Filters")', |
| 'tdl-button:has-text("Apply Filters")', |
| 'button:has-text("Apply Filters")', |
| ].join(', '), |
| backToReport: |
| [ |
| '[id="supplement-container.viewSettings"] button.tii-similarity-supplement--back-button', |
| '.tii-panel-overlay--is-open button.tii-similarity-supplement--back-button', |
| 'button.tii-similarity-supplement--back-button', |
| '[data-px="FiltersBackToReportClicked"]', |
| '[with-data-px="FiltersBackToReportClicked"]', |
| '[withdatapx="FiltersBackToReportClicked"]', |
| 'button:has-text("Back to Similarity Report")', |
| 'tdl-button:has-text("Back to Similarity Report")', |
| ].join(', '), |
| }, |
|
|
| |
| download: { |
| downloadMenu: |
| [ |
| 'tii-sws-download-btn-mfe tdl-labeled-button', |
| 'tii-sws-download-btn-mfe', |
| 'tii-sws-header [slot="download-btn"]', |
| 'tdl-labeled-button[withdatapx="DownloadMenuClicked"]', |
| '[withdatapx="DownloadMenuClicked"]', |
| 'tdl-labeled-button:has-text("Download")', |
| 'button:has-text("Download")', |
| ].join(', '), |
| currentViewOption: |
| [ |
| '[with-data-px="DownloadOptionCurrentViewClicked"]', |
| '[withdatapx="DownloadOptionCurrentViewClicked"]', |
| '[data-px="DownloadOptionCurrentViewClicked"]', |
| ].join(', '), |
| }, |
|
|
| |
| quota: { |
| warningConfirmButton: |
| 'tii-grn-button:has-text("Continue"), tii-grn-button.accept-button, tdl-button[slot="accept-button"], tdl-button[variety="filled"], tii-grn-button', |
| }, |
| }; |
|
|
| |
| |
| |
|
|
| export const EULA_SELECTORS = [ |
| SELECTORS.eula.agreementModalSaveBtn, |
| SELECTORS.eula.iAgreeButton, |
| SELECTORS.eula.acceptButton, |
| SELECTORS.eula.tdlIAgreeButton, |
| SELECTORS.eula.tdlAcceptButton, |
| SELECTORS.eula.iAgreeContinueLink, |
| ]; |
|
|
| export const EULA_BODY_REGEX = |
| /End User License Agreement|Terms of Use|I Agree|I Accept/i; |
|
|
| export const EULA_DEEP_CLICK_TEXTS = [ |
| 'i agree', |
| 'i accept', |
| 'accept', |
| 'continue', |
| 'save', |
| ]; |
|
|
| export const EULA_BLOCK_REGEX = |
| /Could not access the assignment|End User License Agreement needs to be accepted|EULA needs to be accepted/i; |
|
|
| |
| |
| |
|
|
| export const QUOTA_CONFIRM_LABELS = [ |
| 'Continue to File Upload', |
| 'Continue', |
| 'Confirm', |
| 'I understand', |
| 'OK', |
| 'Yes', |
| ]; |
|
|
| |
| |
| |
|
|
| type Scope = Page | Frame; |
|
|
| |
| |
| |
| export async function readScopeDeepText(scope: Scope): Promise<string> { |
| return scope.evaluate(() => { |
| const roots: (Document | ShadowRoot)[] = [document]; |
| const seen = new Set<Element>(); |
|
|
| for (let i = 0; i < roots.length; i++) { |
| const root = roots[i]; |
| for (const el of Array.from(root.querySelectorAll('*'))) { |
| if (seen.has(el)) continue; |
| seen.add(el); |
| if (el.shadowRoot) roots.push(el.shadowRoot); |
| } |
| } |
|
|
| return roots |
| .map((r) => (r as any).body?.innerText || (r as any).host?.innerText || r.textContent || '') |
| .join(' ') |
| .replace(/\s+/g, ' ') |
| .trim(); |
| }).catch(() => ''); |
| } |
|
|
| |
| |
| |
| export async function readScopeVisibleDeepText(scope: Scope): Promise<string> { |
| return scope.evaluate(() => { |
| const roots: (Document | ShadowRoot)[] = [document]; |
| const seen = new Set<Element>(); |
|
|
| for (let i = 0; i < roots.length; i++) { |
| const root = roots[i]; |
| for (const el of Array.from(root.querySelectorAll('*'))) { |
| if (seen.has(el)) continue; |
| seen.add(el); |
| if (el.shadowRoot) roots.push(el.shadowRoot); |
| } |
| } |
|
|
| const isVisible = (el: Element): boolean => { |
| if (!(el instanceof HTMLElement)) return false; |
| const style = window.getComputedStyle(el); |
| const rect = el.getBoundingClientRect(); |
| return ( |
| style.visibility !== 'hidden' && |
| style.display !== 'none' && |
| rect.width > 0 && |
| rect.height > 0 |
| ); |
| }; |
|
|
| const chunks: string[] = []; |
| for (const root of roots) { |
| for (const el of Array.from(root.querySelectorAll('*'))) { |
| if (!isVisible(el)) continue; |
| const text = (el as HTMLElement).innerText || ''; |
| if (text) chunks.push(text); |
| } |
| } |
|
|
| return chunks.join(' ').replace(/\s+/g, ' ').trim(); |
| }).catch(() => ''); |
| } |
|
|
| |
| |
| |
| export async function scopeHasDeepText( |
| scope: Scope, |
| textMatches: string[], |
| ): Promise<boolean> { |
| const text = await readScopeDeepText(scope); |
| const normalized = text.toLowerCase(); |
| return textMatches.some((m) => normalized.includes(m.toLowerCase())); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function scopeHasVisibleDeepText( |
| scope: Scope, |
| textMatches: string[], |
| ): Promise<boolean> { |
| const text = await readScopeVisibleDeepText(scope); |
| const normalized = text.toLowerCase(); |
| return textMatches.some((m) => normalized.includes(m.toLowerCase())); |
| } |
|
|
| |
| |
| |
| export async function clickVisibleCheckbox(scope: Scope): Promise<boolean> { |
| return scope |
| .evaluate(() => { |
| const isVisible = (el: Element): boolean => { |
| const style = window.getComputedStyle(el); |
| const rect = el.getBoundingClientRect(); |
| return ( |
| style.visibility !== 'hidden' && |
| style.display !== 'none' && |
| rect.width > 0 && |
| rect.height > 0 |
| ); |
| }; |
|
|
| const cb = Array.from( |
| document.querySelectorAll( |
| 'input[type="checkbox"], tdl-checkbox, tii-grn-checkbox', |
| ), |
| ).find(isVisible); |
| if (!cb) return false; |
| (cb as HTMLElement).click(); |
| return true; |
| }) |
| .catch(() => false); |
| } |
|
|
| |
| |
| |
| |
| export async function deepClickByText( |
| scope: Scope, |
| textMatches: string[], |
| ): Promise<boolean> { |
| return scope |
| .evaluate((matches: string[]) => { |
| const normalizedMatches = matches.map((v) => v.toLowerCase()); |
| const roots: (Document | ShadowRoot)[] = [document]; |
| const seen = new Set<Element>(); |
|
|
| for (let i = 0; i < roots.length; i++) { |
| const root = roots[i]; |
| for (const el of Array.from(root.querySelectorAll('*'))) { |
| if (seen.has(el)) continue; |
| seen.add(el); |
| if (el.shadowRoot) roots.push(el.shadowRoot); |
| } |
| } |
|
|
| const isVisible = (el: Element): boolean => { |
| if (!(el instanceof HTMLElement)) return false; |
| const style = window.getComputedStyle(el); |
| const rect = el.getBoundingClientRect(); |
| return ( |
| style.visibility !== 'hidden' && |
| style.display !== 'none' && |
| rect.width > 0 && |
| rect.height > 0 |
| ); |
| }; |
|
|
| const candidates: Element[] = []; |
| for (const root of roots) { |
| candidates.push( |
| ...Array.from( |
| root.querySelectorAll( |
| 'button, a, input, tdl-button, tii-grn-button, tdl-labeled-button', |
| ), |
| ), |
| ); |
| } |
|
|
| const target = candidates.find((el) => { |
| if (!isVisible(el)) return false; |
| const text = [ |
| (el as HTMLElement).innerText, |
| el.textContent, |
| el.getAttribute('aria-label'), |
| el.getAttribute('value'), |
| ] |
| .filter(Boolean) |
| .join(' ') |
| .replace(/\s+/g, ' ') |
| .trim() |
| .toLowerCase(); |
| return normalizedMatches.some((m) => text.includes(m)); |
| }); |
|
|
| if (!target) return false; |
| (target as HTMLElement).click(); |
| return true; |
| }, textMatches) |
| .catch(() => false); |
| } |
|
|
| |
| |
| |
| |
| export async function findAssignmentScope( |
| page: Page, |
| selector: string, |
| timeoutMs = 1000, |
| ): Promise<Page | Frame | null> { |
| if ( |
| await page |
| .locator(selector) |
| .first() |
| .isVisible({ timeout: timeoutMs }) |
| .catch(() => false) |
| ) |
| return page; |
|
|
| for (const frame of page.frames()) { |
| if (frame.url().includes('cookie-shim')) continue; |
| if ( |
| await frame |
| .locator(selector) |
| .first() |
| .isVisible({ timeout: timeoutMs }) |
| .catch(() => false) |
| ) |
| return frame; |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| |
| export async function findScopeByDeepText( |
| page: Page, |
| textMatches: string[], |
| timeoutMs = 30000, |
| ): Promise<Page | Frame | null> { |
| const deadline = Date.now() + timeoutMs; |
| while (Date.now() < deadline) { |
| if (await scopeHasDeepText(page, textMatches)) return page; |
|
|
| for (const frame of page.frames()) { |
| if (frame.url().includes('cookie-shim')) continue; |
| if (await scopeHasDeepText(frame, textMatches)) return frame; |
| } |
|
|
| await page.waitForTimeout(1000); |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| export async function resolveAssignmentScope( |
| page: Page, |
| ): Promise<Page | Frame> { |
| return ( |
| (await findAssignmentScope( |
| page, |
| 'button.link-button[aria-label*="View submission"], tii-mpdd-similarity-display', |
| 1000, |
| )) || |
| (await findAssignmentScope( |
| page, |
| 'tdl-button[part="upload-form-container-button"], button:has-text("Browse Files"), input[type="file"]', |
| 1000, |
| )) || |
| page |
| ); |
| } |
|
|