File size: 3,079 Bytes
2db5489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import type { Page } from 'playwright';
import { logger } from '../../utils/logger';
import { SELECTORS } from '../selectors';
import { acceptEulaEverywhere } from './login';

export interface DropClassResult {
  attempted: boolean;
  dropped: boolean;
  reason?: string;
}

/**
 * Drop a class from the Turnitin student home page. This is used only after a
 * hard submission quota limit so exhausted accounts do not keep showing stale
 * class entries during future quota scans.
 */
export async function dropClassByTitle(
  page: Page,
  classTitle: string,
): Promise<DropClassResult> {
  if (!classTitle || classTitle.trim() === '') {
    return {
      attempted: false,
      dropped: false,
      reason: 'Class title is empty',
    };
  }

  logger.info('Attempting to drop quota-limited class', { classTitle });

  await page.goto('https://www.turnitin.com/s_home.asp?lang=en_us', {
    waitUntil: 'domcontentloaded',
    timeout: 60000,
  });
  await page.waitForTimeout(1500);
  await acceptEulaEverywhere(page);

  const classLink = page
    .locator(SELECTORS.class.classNameLink)
    .filter({ hasText: classTitle })
    .first();

  if (!(await classLink.isVisible({ timeout: 10000 }).catch(() => false))) {
    logger.info('Class is already absent from Turnitin home', { classTitle });
    return {
      attempted: true,
      dropped: true,
      reason: 'Class already absent',
    };
  }

  const classRow = page.locator('tr').filter({ hasText: classTitle }).first();
  const deleteLink = classRow
    .locator('td.class_delete a, a[href*="confirmDropClass"]')
    .first();

  if (!(await deleteLink.isVisible({ timeout: 5000 }).catch(() => false))) {
    logger.warn('Class delete link was not found', { classTitle });
    return {
      attempted: true,
      dropped: false,
      reason: 'Delete link not found',
    };
  }

  let dialogAccepted = false;
  const dialogHandler = async (dialog: any) => {
    dialogAccepted = true;
    await dialog.accept().catch(() => {});
  };

  page.once('dialog', dialogHandler);
  await Promise.all([
    page
      .waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 })
      .catch(() => null),
    deleteLink.click({ force: true }),
  ]);
  await page.waitForTimeout(2500);

  if (!dialogAccepted) {
    logger.warn('Drop class confirmation dialog was not observed', {
      classTitle,
    });
  }

  await page.goto('https://www.turnitin.com/s_home.asp?lang=en_us', {
    waitUntil: 'domcontentloaded',
    timeout: 60000,
  });
  await page.waitForTimeout(1500);

  const stillVisible = await page
    .locator(SELECTORS.class.classNameLink)
    .filter({ hasText: classTitle })
    .first()
    .isVisible({ timeout: 5000 })
    .catch(() => false);

  if (stillVisible) {
    logger.warn('Class still visible after delete attempt', { classTitle });
    return {
      attempted: true,
      dropped: false,
      reason: 'Class still visible after delete',
    };
  }

  logger.info('Quota-limited class dropped successfully', { classTitle });
  return { attempted: true, dropped: true };
}