dceky 1.0.0-beta-yuenler.2 → 1.0.0-beta-auto-import.1
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.
- package/.eslintrc.js +93 -0
- package/lib/commands/assertDoesNotHaveClass.d.ts +2 -1
- package/lib/commands/assertDoesNotHaveClass.js.map +1 -1
- package/lib/commands/assertHasClass.d.ts +2 -1
- package/lib/commands/assertHasClass.js.map +1 -1
- package/lib/commands/assertNumElements.d.ts +2 -1
- package/lib/commands/assertNumElements.js.map +1 -1
- package/lib/commands/clickWithRetry.d.ts +8 -4
- package/lib/commands/clickWithRetry.js +3 -3
- package/lib/commands/clickWithRetry.js.map +1 -1
- package/lib/commands/getNumElements.d.ts +4 -4
- package/lib/commands/getNumElements.js +1 -1
- package/lib/commands/getNumElements.js.map +1 -1
- package/lib/commands/handleHarvardKey.d.ts +9 -3
- package/lib/commands/handleHarvardKey.js +19 -22
- package/lib/commands/handleHarvardKey.js.map +1 -1
- package/lib/commands/handleHarvardKey2.d.ts +14 -0
- package/lib/commands/handleHarvardKey2.js +88 -0
- package/lib/commands/handleHarvardKey2.js.map +1 -0
- package/lib/commands/launchAs.d.ts +14 -12
- package/lib/commands/launchAs.js +24 -23
- package/lib/commands/launchAs.js.map +1 -1
- package/lib/commands/launchLTIUsingToken.d.ts +9 -4
- package/lib/commands/launchLTIUsingToken.js +25 -14
- package/lib/commands/launchLTIUsingToken.js.map +1 -1
- package/lib/commands/navigateToHref.d.ts +4 -9
- package/lib/commands/navigateToHref.js +10 -3
- package/lib/commands/navigateToHref.js.map +1 -1
- package/lib/commands/runScript.d.ts +3 -3
- package/lib/commands/runScript.js +2 -6
- package/lib/commands/runScript.js.map +1 -1
- package/lib/commands/typeInto.d.ts +5 -2
- package/lib/commands/typeInto.js +14 -4
- package/lib/commands/typeInto.js.map +1 -1
- package/lib/commands/visitCanvasGETEndpoint.d.ts +2 -2
- package/lib/commands/visitCanvasGETEndpoint.js +4 -4
- package/lib/commands/visitCanvasGETEndpoint.js.map +1 -1
- package/lib/commands/waitForElementVisible.d.ts +3 -2
- package/lib/commands/waitForElementVisible.js +1 -2
- package/lib/commands/waitForElementVisible.js.map +1 -1
- package/lib/genCommandPaths.d.ts +9 -0
- package/lib/genCommandPaths.js +86 -0
- package/lib/genCommandPaths.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +3 -3
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +1 -5
- package/lib/init.js +71 -33
- package/lib/init.js.map +1 -1
- package/package.json +8 -2
- package/src/commands/assertDoesNotHaveClass.ts +20 -9
- package/src/commands/assertHasClass.ts +20 -9
- package/src/commands/assertNumElements.ts +20 -6
- package/src/commands/getNumElements.ts +17 -10
- package/src/commands/handleHarvardKey.ts +68 -53
- package/src/commands/launchAs.ts +79 -63
- package/src/commands/launchLTIUsingToken.ts +78 -47
- package/src/commands/navigateToHref.ts +28 -17
- package/src/commands/runScript.ts +16 -11
- package/src/commands/typeInto.ts +54 -26
- package/src/commands/visitCanvasGETEndpoint.ts +29 -16
- package/src/commands/waitForElementVisible.ts +19 -10
- package/src/genCommandPaths.ts +65 -0
- package/src/index.ts +2 -3
- package/src/init.ts +80 -34
- package/.eslintrc.json +0 -58
- package/src/commands/clickWithRetry.ts +0 -51
|
@@ -9,15 +9,18 @@ declare global {
|
|
|
9
9
|
interface Chainable {
|
|
10
10
|
/**
|
|
11
11
|
* Assert that an element does not have a specific className
|
|
12
|
+
* @author Yuen Ler Chow
|
|
12
13
|
* @param opts object containing all arguments
|
|
13
14
|
* @param opts.item the css selector for finding the element
|
|
14
15
|
* @param opts.className the className to check for
|
|
15
|
-
* @
|
|
16
|
+
* @return Cypress chainable containing the element that was checked
|
|
16
17
|
*/
|
|
17
|
-
assertDoesNotHaveClass(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
assertDoesNotHaveClass(
|
|
19
|
+
opts: {
|
|
20
|
+
item: string,
|
|
21
|
+
className: string,
|
|
22
|
+
},
|
|
23
|
+
): Chainable<JQuery<HTMLElement>>;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -27,10 +30,18 @@ declare global {
|
|
|
27
30
|
/*----------------------------------------*/
|
|
28
31
|
|
|
29
32
|
const assertDoesNotHaveClass = () => {
|
|
30
|
-
Cypress.Commands.add(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
Cypress.Commands.add(
|
|
34
|
+
'assertDoesNotHaveClass',
|
|
35
|
+
(
|
|
36
|
+
opts: {
|
|
37
|
+
item: string,
|
|
38
|
+
className: string,
|
|
39
|
+
},
|
|
40
|
+
) => {
|
|
41
|
+
cy.log(`Assert ${opts.item} does not have class ${opts.className}`);
|
|
42
|
+
return cy.get(opts.item).should('not.have.class', opts.className);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
34
45
|
};
|
|
35
46
|
|
|
36
47
|
/*----------------------------------------*/
|
|
@@ -9,15 +9,18 @@ declare global {
|
|
|
9
9
|
interface Chainable {
|
|
10
10
|
/**
|
|
11
11
|
* Assert that an element has a specific className
|
|
12
|
+
* @author Yuen Ler Chow
|
|
12
13
|
* @param opts object containing all arguments
|
|
13
14
|
* @param opts.item the css selector for finding the element
|
|
14
15
|
* @param opts.className the className to check for
|
|
15
|
-
* @
|
|
16
|
+
* @return Cypress chainable containing the element that was checked
|
|
16
17
|
*/
|
|
17
|
-
assertHasClass(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
assertHasClass(
|
|
19
|
+
opts: {
|
|
20
|
+
item: string,
|
|
21
|
+
className: string,
|
|
22
|
+
},
|
|
23
|
+
): Chainable<JQuery<HTMLElement>>;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -27,10 +30,18 @@ declare global {
|
|
|
27
30
|
/*----------------------------------------*/
|
|
28
31
|
|
|
29
32
|
const assertHasClass = () => {
|
|
30
|
-
Cypress.Commands.add(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
Cypress.Commands.add(
|
|
34
|
+
'assertHasClass',
|
|
35
|
+
(
|
|
36
|
+
opts: {
|
|
37
|
+
item: string,
|
|
38
|
+
className: string,
|
|
39
|
+
},
|
|
40
|
+
) => {
|
|
41
|
+
cy.log(`Assert ${opts.item} has class ${opts.className}`);
|
|
42
|
+
return cy.get(opts.item).should('have.class', opts.className);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
34
45
|
};
|
|
35
46
|
|
|
36
47
|
/*----------------------------------------*/
|
|
@@ -9,12 +9,18 @@ declare global {
|
|
|
9
9
|
interface Chainable {
|
|
10
10
|
/**
|
|
11
11
|
* Assert a certain number of elements
|
|
12
|
+
* @author Yuen Ler Chow
|
|
12
13
|
* @param opts object containing all arguments
|
|
13
14
|
* @param opts.item a CSS selector corresponding to the item
|
|
14
15
|
* @param opts.num the precise number of elements expected
|
|
15
|
-
* @
|
|
16
|
+
* @return Cypress chainable
|
|
16
17
|
*/
|
|
17
|
-
assertNumElements(
|
|
18
|
+
assertNumElements(
|
|
19
|
+
opts: {
|
|
20
|
+
item: string,
|
|
21
|
+
num: number,
|
|
22
|
+
}
|
|
23
|
+
): Chainable<Element>;
|
|
18
24
|
}
|
|
19
25
|
}
|
|
20
26
|
}
|
|
@@ -24,10 +30,18 @@ declare global {
|
|
|
24
30
|
/*----------------------------------------*/
|
|
25
31
|
|
|
26
32
|
const assertNumElements = () => {
|
|
27
|
-
Cypress.Commands.add(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
Cypress.Commands.add(
|
|
34
|
+
'assertNumElements',
|
|
35
|
+
(
|
|
36
|
+
opts: {
|
|
37
|
+
item: string,
|
|
38
|
+
num: number,
|
|
39
|
+
},
|
|
40
|
+
) => {
|
|
41
|
+
cy.log(`Assert ${opts.num} elements match ${opts.item}`);
|
|
42
|
+
cy.get(opts.item).should('have.length', opts.num);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
31
45
|
};
|
|
32
46
|
|
|
33
47
|
/*----------------------------------------*/
|
|
@@ -9,11 +9,13 @@ declare global {
|
|
|
9
9
|
interface Chainable {
|
|
10
10
|
/**
|
|
11
11
|
* Get number of elements
|
|
12
|
-
* @
|
|
13
|
-
* @
|
|
14
|
-
* @
|
|
12
|
+
* @author Yuen Ler Chow
|
|
13
|
+
* @param selector a CSS selector corresponding to the item
|
|
14
|
+
* @return Cypress chainable containing the number of elements matching the selector
|
|
15
15
|
*/
|
|
16
|
-
getNumElements(
|
|
16
|
+
getNumElements(
|
|
17
|
+
selector: string,
|
|
18
|
+
): Chainable<number>;
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -23,12 +25,17 @@ declare global {
|
|
|
23
25
|
/*----------------------------------------*/
|
|
24
26
|
|
|
25
27
|
const getNumElements = () => {
|
|
26
|
-
Cypress.Commands.add(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
Cypress.Commands.add(
|
|
29
|
+
'getNumElements',
|
|
30
|
+
(
|
|
31
|
+
selector: string,
|
|
32
|
+
) => {
|
|
33
|
+
cy.log(`Count elements matching ${selector}`);
|
|
34
|
+
return cy.get(selector).then(($elements: JQuery<HTMLElement>) => {
|
|
35
|
+
return Cypress.$($elements).length;
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
);
|
|
32
39
|
};
|
|
33
40
|
|
|
34
41
|
/*----------------------------------------*/
|
|
@@ -9,10 +9,18 @@ declare global {
|
|
|
9
9
|
interface Chainable {
|
|
10
10
|
/**
|
|
11
11
|
* Handle a HarvardKey login page for a user
|
|
12
|
-
* @
|
|
13
|
-
* @
|
|
12
|
+
* @author Yuen Ler Chow
|
|
13
|
+
* @param opts object containing all arguments
|
|
14
|
+
* @param opts.url the URL to visit for HarvardKey authentication
|
|
15
|
+
* @param opts.name the name of the user environment variable
|
|
16
|
+
* @return Cypress chainable (void) - performs authentication flow, no return value
|
|
14
17
|
*/
|
|
15
|
-
handleHarvardKey(
|
|
18
|
+
handleHarvardKey(
|
|
19
|
+
opts: {
|
|
20
|
+
url: string,
|
|
21
|
+
name: string,
|
|
22
|
+
}
|
|
23
|
+
): Chainable<void>;
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
}
|
|
@@ -22,62 +30,69 @@ declare global {
|
|
|
22
30
|
/*----------------------------------------*/
|
|
23
31
|
|
|
24
32
|
const handleHarvardKey = () => {
|
|
25
|
-
Cypress.Commands.add(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!password) {
|
|
37
|
-
throw new Error(`HARVARD_PASSWORD environment variable is not set`);
|
|
38
|
-
}
|
|
33
|
+
Cypress.Commands.add(
|
|
34
|
+
'handleHarvardKey',
|
|
35
|
+
(
|
|
36
|
+
opts: {
|
|
37
|
+
url: string,
|
|
38
|
+
name: string,
|
|
39
|
+
},
|
|
40
|
+
) => {
|
|
41
|
+
const { url, name } = opts;
|
|
42
|
+
cy.log('Handling HarvardKey authentication');
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
const userInfo = Cypress.env(name);
|
|
45
|
+
if (!userInfo) {
|
|
46
|
+
throw new Error(`Could not find ${name} in environment variables`);
|
|
47
|
+
}
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
// Destructure the user info object to get the username and password
|
|
50
|
+
const { username } = userInfo;
|
|
51
|
+
const { password } = userInfo;
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}).then((fullUrl: any) => {
|
|
51
|
-
// Navigate to the Harvard login page using originWithKaixa
|
|
52
|
-
cy.origin('https://harvard.idp.cirrusidentity.com', {
|
|
53
|
-
args: { username, password, fullUrl }
|
|
54
|
-
}, (args) => {
|
|
53
|
+
cy.visit(url);
|
|
54
|
+
|
|
55
|
+
// Get the Harvard login URL using originWithKaixa with auto-initialized functions
|
|
56
|
+
cy.origin('https://apps.cirrusidentity.com', () => {
|
|
55
57
|
Cypress.require('dceky');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
// Clicking the button doesn't work for some reason, so we access the href attribute instead
|
|
59
|
+
return cy.navigateToHref('#idp_1001962798_button');
|
|
60
|
+
}).then((fullUrl: string) => {
|
|
61
|
+
// Navigate to the Harvard login page using originWithKaixa
|
|
62
|
+
cy.origin('https://harvard.idp.cirrusidentity.com', {
|
|
63
|
+
args: { username, password, fullUrl },
|
|
64
|
+
}, (args) => {
|
|
65
|
+
Cypress.require('dceky');
|
|
66
|
+
const {
|
|
67
|
+
username: harvardUsername,
|
|
68
|
+
password: harvardPassword,
|
|
69
|
+
fullUrl: harvardLoginUrl,
|
|
70
|
+
} = args;
|
|
71
|
+
|
|
72
|
+
cy.on('uncaught:exception', (e: Error) => {
|
|
73
|
+
// Ignore known HarvardKey errors
|
|
74
|
+
if (!e.message.includes('ready is not defined')) {
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Visit the Harvard login page
|
|
81
|
+
cy.visit(harvardLoginUrl);
|
|
82
|
+
|
|
83
|
+
// Wait for and fill in the login form
|
|
84
|
+
cy.waitForElementVisible('#username');
|
|
85
|
+
cy.waitForElementVisible('#password');
|
|
86
|
+
|
|
87
|
+
cy.typeInto({ item: '#username', text: harvardUsername });
|
|
88
|
+
cy.typeInto({ item: '#password', text: harvardPassword });
|
|
89
|
+
|
|
90
|
+
// Submit the form
|
|
91
|
+
cy.get('input[type="submit"], button[type="submit"], .btn-primary').click();
|
|
64
92
|
});
|
|
65
|
-
|
|
66
|
-
cy.visit(fullUrl.toString());
|
|
67
|
-
|
|
68
|
-
// Wait for and fill in the login form using enhanced functions
|
|
69
|
-
cy.waitForElementVisible('#username');
|
|
70
|
-
cy.waitForElementVisible('#password');
|
|
71
|
-
|
|
72
|
-
// Add credentials using enhanced functions
|
|
73
|
-
cy.typeInto({ item: '#username', text: username });
|
|
74
|
-
cy.typeInto({ item: '#password', text: password });
|
|
75
|
-
|
|
76
|
-
// Submit the form using clickWithRetry
|
|
77
|
-
cy.clickWithRetry('input[type="submit"], button[type="submit"], .btn-primary');
|
|
78
93
|
});
|
|
79
|
-
}
|
|
80
|
-
|
|
94
|
+
},
|
|
95
|
+
);
|
|
81
96
|
};
|
|
82
97
|
|
|
83
98
|
/*----------------------------------------*/
|
package/src/commands/launchAs.ts
CHANGED
|
@@ -9,19 +9,24 @@ declare global {
|
|
|
9
9
|
interface Chainable {
|
|
10
10
|
/**
|
|
11
11
|
* Log into Canvas and launch an LTI app as a specific user from environment variables.
|
|
12
|
-
* The user should be defined as environment
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* @
|
|
22
|
-
* @example cy.launchAs('student1', 12345, 'My LTI App')
|
|
12
|
+
* The user should be defined as an environment variable with the following properties:
|
|
13
|
+
* - accessToken: Canvas access token for the user (required for production mode)
|
|
14
|
+
* - type: User type ('student', 'ta', 'teacher') for local mode (defaults to 'teacher')
|
|
15
|
+
* - simIndex: Simulator index for local mode (defaults to '0')
|
|
16
|
+
* @author Yuen Ler Chow
|
|
17
|
+
* @param name the name of the user environment variable
|
|
18
|
+
* @param opts object containing all arguments
|
|
19
|
+
* @param [opts.courseId] the Canvas ID of the course to launch from
|
|
20
|
+
* @param [opts.appName] the name of the app as it appears in the course's left-hand nav
|
|
21
|
+
* @return Cypress chainable (void) - launches the LTI app, no return value
|
|
23
22
|
*/
|
|
24
|
-
launchAs(
|
|
23
|
+
launchAs(
|
|
24
|
+
name: string,
|
|
25
|
+
opts?: {
|
|
26
|
+
courseId?: number,
|
|
27
|
+
appName?: string,
|
|
28
|
+
}
|
|
29
|
+
): Chainable<void>;
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
}
|
|
@@ -31,70 +36,81 @@ declare global {
|
|
|
31
36
|
/*----------------------------------------*/
|
|
32
37
|
|
|
33
38
|
const launchAs = () => {
|
|
34
|
-
Cypress.Commands.add(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
Cypress.Commands.add(
|
|
40
|
+
'launchAs',
|
|
41
|
+
(
|
|
42
|
+
name: string,
|
|
43
|
+
opts: {
|
|
44
|
+
courseId?: number,
|
|
45
|
+
appName?: string,
|
|
46
|
+
} = {},
|
|
47
|
+
) => {
|
|
48
|
+
let { courseId, appName } = opts;
|
|
49
|
+
|
|
50
|
+
// Get courseId from environment variables if not provided
|
|
51
|
+
if (!courseId) {
|
|
52
|
+
const envCourseId = Cypress.env('courseId');
|
|
53
|
+
if (!envCourseId) {
|
|
54
|
+
throw new Error('Could not find courseId in environment variables');
|
|
55
|
+
}
|
|
56
|
+
courseId = Number.parseInt(envCourseId, 10);
|
|
42
57
|
}
|
|
43
|
-
courseId = parseInt(envCourseId, 10);
|
|
44
|
-
}
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
appName = Cypress.env('
|
|
59
|
+
// Get appName from environment variables if not provided
|
|
60
|
+
appName = appName || Cypress.env('appName');
|
|
48
61
|
if (!appName) {
|
|
49
|
-
throw new Error('
|
|
62
|
+
throw new Error('Could not find appName in environment variables');
|
|
50
63
|
}
|
|
51
|
-
}
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
// Get user info from environment variables
|
|
66
|
+
const userInfo = Cypress.env(name);
|
|
67
|
+
if (!userInfo) {
|
|
68
|
+
throw new Error(`Could not find ${name} in environment variables`);
|
|
69
|
+
}
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
// Handle local simulator launch
|
|
58
|
-
const userType = Cypress.env(`${name}_TYPE`) || 'teacher';
|
|
59
|
-
const simIndex = Cypress.env(`${name}_SIM_INDEX`) || '0';
|
|
60
|
-
const simLaunchButtonId = `${userType}_${simIndex}-launch-button`;
|
|
71
|
+
cy.log(`🚀 Launch as ${name}`);
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
// Check if this is a local launch
|
|
74
|
+
const isLocal = Cypress.env('local') === 'true';
|
|
64
75
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
cy.get('#proceed-link').click();
|
|
71
|
-
}
|
|
72
|
-
});
|
|
76
|
+
if (isLocal) {
|
|
77
|
+
// Handle local simulator launch
|
|
78
|
+
const userType = userInfo.type || 'teacher';
|
|
79
|
+
const simIndex = userInfo.simIndex || '0';
|
|
80
|
+
const simLaunchButtonId = `${userType}_${simIndex}-launch-button`;
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
cy.log('Local mode: launching simulator');
|
|
83
|
+
cy.visit('https://localhost:8088/simulator');
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
// Handle potential SSL certificate issues
|
|
86
|
+
cy.get('body').then(($body) => {
|
|
87
|
+
if ($body.find('.ssl').length > 0) {
|
|
88
|
+
cy.log('Handling SSL certificate issue');
|
|
89
|
+
cy.get('#details-button').click();
|
|
90
|
+
cy.get('#proceed-link').click();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
// Launch the app
|
|
95
|
+
cy.get(`#${simLaunchButtonId}`).click();
|
|
87
96
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
// Click the authorize button
|
|
98
|
+
cy.get('.authorize-button', { timeout: 1000 }).click();
|
|
99
|
+
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle production launch with access token
|
|
104
|
+
const { accessToken } = userInfo;
|
|
105
|
+
|
|
106
|
+
if (!accessToken) {
|
|
107
|
+
throw new Error(`Could not find accessToken for ${name} in environment variables`);
|
|
108
|
+
}
|
|
94
109
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
// Launch using the access token
|
|
111
|
+
cy.launchLTIUsingToken(accessToken, { courseId, appName });
|
|
112
|
+
},
|
|
113
|
+
);
|
|
98
114
|
};
|
|
99
115
|
|
|
100
116
|
/*----------------------------------------*/
|
|
@@ -9,12 +9,20 @@ declare global {
|
|
|
9
9
|
interface Chainable {
|
|
10
10
|
/**
|
|
11
11
|
* Log into Canvas using an access token and launch an LTI app
|
|
12
|
+
* @author Yuen Ler Chow
|
|
12
13
|
* @param accessToken the user's Canvas access token
|
|
13
|
-
* @param
|
|
14
|
-
* @param
|
|
15
|
-
* @
|
|
14
|
+
* @param opts object containing all arguments
|
|
15
|
+
* @param [opts.courseId] the Canvas ID of the course to launch from
|
|
16
|
+
* @param [opts.appName] the name of the app as it appears in the course's left-hand nav
|
|
17
|
+
* @return Cypress chainable (void) - launches the LTI app and navigates to it, no return value
|
|
16
18
|
*/
|
|
17
|
-
launchLTIUsingToken(
|
|
19
|
+
launchLTIUsingToken(
|
|
20
|
+
accessToken: string,
|
|
21
|
+
opts?: {
|
|
22
|
+
courseId?: number,
|
|
23
|
+
appName?: string,
|
|
24
|
+
}
|
|
25
|
+
): Chainable<void>;
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
}
|
|
@@ -24,57 +32,80 @@ declare global {
|
|
|
24
32
|
/*----------------------------------------*/
|
|
25
33
|
|
|
26
34
|
const launchLTIUsingToken = () => {
|
|
27
|
-
Cypress.Commands.add(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Find the external tool of interest
|
|
38
|
-
let toolId = 0;
|
|
39
|
-
|
|
40
|
-
for (const externalTool of externalTools) {
|
|
41
|
-
// Skip non-nav items
|
|
42
|
-
if (!externalTool.course_navigation || typeof externalTool.course_navigation !== 'object') {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Skip non-labeled items
|
|
47
|
-
if (!externalTool.course_navigation.text) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
35
|
+
Cypress.Commands.add(
|
|
36
|
+
'launchLTIUsingToken',
|
|
37
|
+
(
|
|
38
|
+
accessToken: string,
|
|
39
|
+
opts: {
|
|
40
|
+
courseId?: number,
|
|
41
|
+
appName?: string,
|
|
42
|
+
} = {},
|
|
43
|
+
) => {
|
|
44
|
+
let { courseId, appName } = opts;
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
cy.log(`Found matching app: "${externalTool.course_navigation.text}" with ID ${toolId}`);
|
|
56
|
-
break;
|
|
46
|
+
if (!courseId) {
|
|
47
|
+
const envCourseId = Cypress.env('courseId');
|
|
48
|
+
if (!envCourseId) {
|
|
49
|
+
throw new Error('Could not find courseId in environment variables');
|
|
57
50
|
}
|
|
51
|
+
courseId = Number.parseInt(envCourseId, 10);
|
|
58
52
|
}
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
throw new Error(
|
|
54
|
+
appName = appName || Cypress.env('appName');
|
|
55
|
+
if (!appName) {
|
|
56
|
+
throw new Error('Could not find appName in environment variables');
|
|
63
57
|
}
|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
cy.log(`Launch LTI app "${appName}" in course ${courseId} using access token`);
|
|
60
|
+
|
|
61
|
+
// Get the external tools for the course
|
|
66
62
|
cy.visitCanvasGETEndpoint({
|
|
67
|
-
path: `/courses/${courseId}/external_tools
|
|
68
|
-
accessToken
|
|
69
|
-
}).then((
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
path: `/courses/${courseId}/external_tools`,
|
|
64
|
+
accessToken,
|
|
65
|
+
}).then((externalTools: { [key: string]: any }[]) => {
|
|
66
|
+
cy.log(`Found ${externalTools.length} external tools`);
|
|
67
|
+
|
|
68
|
+
// Find the external tool of interest
|
|
69
|
+
const matchingTool = externalTools.find((externalTool) => {
|
|
70
|
+
// Skip non-nav items
|
|
71
|
+
if (!externalTool.course_navigation || typeof externalTool.course_navigation !== 'object') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Skip non-labeled items
|
|
76
|
+
if (!externalTool.course_navigation.text) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Skip if the app name does not match the expected name
|
|
81
|
+
const thisAppName = externalTool.course_navigation.text.trim().toLowerCase();
|
|
82
|
+
return thisAppName === appName.trim().toLowerCase();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const toolId = matchingTool?.id || 0;
|
|
86
|
+
if (matchingTool) {
|
|
87
|
+
cy.log(`Found matching app: "${matchingTool.course_navigation.text}" with ID ${toolId}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Make sure we found the app
|
|
91
|
+
if (toolId === 0) {
|
|
92
|
+
throw new Error(`Could not find any apps named "${appName}" in course ${courseId}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get a sessionless launch URL
|
|
96
|
+
cy.visitCanvasGETEndpoint({
|
|
97
|
+
path: `/courses/${courseId}/external_tools/sessionless_launch?id=${toolId}`,
|
|
98
|
+
accessToken,
|
|
99
|
+
}).then((sessionlessLaunchInfo) => {
|
|
100
|
+
const launchURL = sessionlessLaunchInfo.url;
|
|
101
|
+
cy.log(`Launching LTI app at: ${launchURL}`);
|
|
102
|
+
|
|
103
|
+
// Launch the tool
|
|
104
|
+
cy.visit(launchURL);
|
|
105
|
+
});
|
|
75
106
|
});
|
|
76
|
-
}
|
|
77
|
-
|
|
107
|
+
},
|
|
108
|
+
);
|
|
78
109
|
};
|
|
79
110
|
|
|
80
111
|
/*----------------------------------------*/
|