Skip to content

Commit d1b7515

Browse files
authored
Merge pull request #1939 from RedisInsight/feature/RI-4318-tutorials_images
Feature/ri 4318 tutorials images
2 parents 4731b61 + 6704d26 commit d1b7515

File tree

7 files changed

+105
-29
lines changed

7 files changed

+105
-29
lines changed

redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/LazyInternalPage/LazyInternalPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const LazyInternalPage = ({ onClose, title, path, sourcePath, manifest, manifest
6464
const { data, status } = await fetchService.get<string>(path)
6565
if (isStatusSuccessful(status)) {
6666
dispatch(setWorkbenchEASearch(search))
67-
const contentData = await formatter.format(data, { history })
67+
const contentData = await formatter.format({ data, path }, { history })
6868
setPageData((prevState) => ({ ...prevState, content: contentData }))
6969
setLoading(false)
7070
}

redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/formatter/MarkdownToJsxString.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import { remarkRedisCode } from '../transform/remarkRedisCode'
1111
import { remarkImage } from '../transform/remarkImage'
1212

1313
class MarkdownToJsxString implements IFormatter {
14-
format(data: any, config?: IFormatterConfig): Promise<string> {
14+
format(input: any, config?: IFormatterConfig): Promise<string> {
15+
const { data, path } = input
1516
return new Promise((resolve, reject) => {
1617
unified()
1718
.use(remarkParse)
1819
.use(remarkGfm) // support GitHub Flavored Markdown
1920
.use(remarkRedisCode) // Add custom component for Redis code block
20-
.use(remarkImage, config ? { history: config.history } : undefined) // Add custom component for Redis code block
21+
.use(remarkImage, path) // Add custom component for Redis code block
2122
.use(remarkRehype, { allowDangerousHtml: true }) // Pass raw HTML strings through.
2223
.use(rehypeLinks, config ? { history: config.history } : undefined) // Customise links
2324
.use(MarkdownToJsxString.rehypeWrapSymbols) // Wrap special symbols inside curly braces for JSX parse
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { visit } from 'unist-util-visit'
2+
import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService'
3+
import { remarkImage } from '../transform/remarkImage'
4+
5+
jest.mock('unist-util-visit')
6+
const TUTORIAL_PATH = 'static/custom-tutorials/tutorial-id'
7+
const testCases = [
8+
{
9+
url: '../../../_images/relative.png',
10+
path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,
11+
result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`,
12+
},
13+
{
14+
url: '/../../../_images/relative.png', // NOTE: will not work in real. There is no sense to even support absolute paths
15+
path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,
16+
result: `${RESOURCES_BASE_URL}_images/relative.png`,
17+
},
18+
{
19+
url: 'https://somesite.test/image.png',
20+
path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,
21+
result: 'https://somesite.test/image.png',
22+
}
23+
]
24+
describe('remarkImage', () => {
25+
testCases.forEach((tc) => {
26+
it(`should return ${tc.result} for url:${tc.url}, path: ${tc.path} `, () => {
27+
const node = {
28+
url: tc.url,
29+
};
30+
31+
// mock implementation
32+
(visit as jest.Mock)
33+
.mockImplementation((_tree: any, _name: string, callback: (node: any) => void) => { callback(node) })
34+
35+
const remark = remarkImage(tc.path)
36+
remark({} as Node)
37+
expect(node).toEqual({
38+
...node,
39+
url: tc.result,
40+
})
41+
})
42+
})
43+
})
Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
11
import { visit } from 'unist-util-visit'
2-
import { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'
32
import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService'
4-
import { ApiEndpoints } from 'uiSrc/constants'
5-
import { IFormatterConfig } from './formatter/formatter.interfaces'
63

7-
const getSourcelPath = (search?: string) => {
8-
switch (true) {
9-
case search?.indexOf(ApiEndpoints.GUIDES_PATH) !== -1:
10-
return 'static/guides/'
11-
case search?.indexOf(ApiEndpoints.TUTORIALS_PATH) !== -1:
12-
return 'static/tutorials/'
13-
default:
14-
return ''
15-
}
16-
}
17-
18-
const updateUrl = (url: string) => url.replace(/^\//, '')
19-
20-
export const remarkImage = (config?: IFormatterConfig): (tree: Node) => void => (tree: any) => {
21-
const sourcePath = getSourcelPath(config?.history?.location?.search)
4+
export const remarkImage = (path: string): (tree: Node) => void => (tree: any) => {
225
// Find img node in syntax tree
236
visit(tree, 'image', (node) => {
24-
node.url = IS_ABSOLUTE_PATH.test(node.url || '') ? node.url : `${RESOURCES_BASE_URL}${sourcePath}${updateUrl(node.url)}`
7+
const pathURL = new URL(path, RESOURCES_BASE_URL)
8+
const url = new URL(node.url, pathURL)
9+
node.url = url.toString()
2510
})
2611
}

tests/e2e/pageObjects/workbench-page.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export class WorkbenchPage {
3333
customTutorials = Selector('[data-testid=accordion-button-custom-tutorials]');
3434
tutorialOpenUploadButton = Selector('[data-testid=open-upload-tutorial-btn]');
3535
tutorialLinkField = Selector('[data-testid=tutorial-link-field]');
36-
tutorialLatestDeleteIcon = Selector('[data-testid^=delete-tutorial-icon-]').nth(0);
37-
tutorialDeleteButton = Selector('[data-testid^=delete-tutorial-]').withText('Delete');
36+
tutorialLatestDeleteIcon = Selector('[data-testid^=delete-tutorial-icon-]').nth(0);
37+
tutorialDeleteButton = Selector('[data-testid^=delete-tutorial-]').withText('Delete');
3838
tutorialNameField = Selector('[data-testid=tutorial-name-field]');
3939
tutorialSubmitButton = Selector('[data-testid=submit-upload-tutorial-btn]');
4040
tutorialImport = Selector('[data-testid=import-tutorial]');
@@ -233,32 +233,59 @@ export class WorkbenchPage {
233233
const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent;
234234
await t.expect(actualCommandResult).contains(result, 'Actual command result is not equal to executed');
235235
}
236+
236237
/**
237238
* Get selector with tutorial name
238239
* @param tutorialName name of the uploaded tutorial
239240
*/
240241
async getAccordionButtonWithName(tutorialName: string): Promise<Selector> {
241242
return Selector(`[data-testid=accordion-button-${tutorialName}]`);
242243
}
244+
243245
/**
244246
* Get internal tutorial link with .md name
245247
* @param internalLink name of the .md file
246248
*/
247249
async getInternalLinkWithManifest(internalLink: string): Promise<Selector> {
248250
return Selector(`[data-testid="internal-link-${internalLink}.md"]`);
249251
}
252+
250253
/**
251254
* Get internal tutorial link without .md name
252255
* @param internalLink name of the label
253256
*/
254257
async getInternalLinkWithoutManifest(internalLink: string): Promise<Selector> {
255258
return Selector(`[data-testid="internal-link-${internalLink}"]`);
256259
}
260+
257261
/**
258262
* Find tutorial selector by name
259263
* @param name A tutorial name
260264
*/
261265
async getTutorialByName(name: string): Promise<Selector> {
262266
return Selector('div').withText(name);
263267
}
268+
269+
/**
270+
* Find image in tutorial by alt text
271+
* @param alt Image alt text
272+
*/
273+
async getTutorialImageByAlt(alt: string): Promise<Selector> {
274+
return Selector('img').withAttribute('alt', alt);
275+
}
276+
277+
/**
278+
* Wait until image rendered
279+
* @param selector Image selector
280+
*/
281+
async waitUntilImageRendered(selector: Selector): Promise<void> {
282+
const searchTimeout = 5 * 1000; // 5 sec maximum wait
283+
const startTime = Date.now();
284+
let imageHeight = await selector.getStyleProperty('height');
285+
286+
do {
287+
imageHeight = await selector.getStyleProperty('height');
288+
}
289+
while ((imageHeight == '0px') && Date.now() - startTime < searchTimeout);
290+
}
264291
}
-600 KB
Binary file not shown.

tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,37 @@ let internalLinkName1 = 'probably-1';
1919
let internalLinkName2 = 'vector-2';
2020

2121
fixture `Upload custom tutorials`
22-
.meta({type: 'regression', rte: rte.standalone})
22+
.meta({ type: 'regression', rte: rte.standalone })
2323
.page(commonUrl)
2424
.beforeEach(async t => {
2525
await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName);
2626
await t.click(myRedisDatabasePage.workbenchButton);
2727
})
28-
.afterEach(async() => {
28+
.afterEach(async () => {
2929
await deleteStandaloneDatabaseApi(ossStandaloneConfig);
3030
});
31-
// https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4198, https://redislabs.atlassian.net/browse/RI-4302
31+
/* https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4198,
32+
https://redislabs.atlassian.net/browse/RI-4302, https://redislabs.atlassian.net/browse/RI-4318
33+
*/
3234
test('Verify that user can upload tutorial with local zip file without manifest.json', async t => {
3335
// Verify that user can upload custom tutorials on docker version
36+
const imageExternalPath = 'RedisInsight screen external';
37+
const imageRelativePath = 'RedisInsight screen relative';
3438
folder1 = 'folder-1';
3539
folder2 = 'folder-2';
3640
internalLinkName1 = 'probably-1';
3741
internalLinkName2 = 'vector-2';
42+
3843
// Verify that user can see the “MY TUTORIALS” section in the Enablement area.
3944
await t.expect(workbenchPage.customTutorials.visible).ok('custom tutorials sections is not visible');
4045
await t.click(workbenchPage.tutorialOpenUploadButton);
4146
await t.expect(workbenchPage.tutorialSubmitButton.hasAttribute('disabled')).ok('submit button is not disabled');
47+
4248
// Verify that User can request to add a new custom Tutorial by uploading a .zip archive from a local folder
4349
await t.setFilesToUpload(workbenchPage.tutorialImport, [filePath]);
4450
await t.click(workbenchPage.tutorialSubmitButton);
4551
await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).visible).ok(`${tutorialName} tutorial is not uploaded`);
52+
4653
// Verify that when user upload a .zip archive without a .json manifest, all markdown files are inserted at the same hierarchy level
4754
await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName));
4855
await t.expect((await workbenchPage.getAccordionButtonWithName(folder1)).visible).ok(`${folder1} is not visible`);
@@ -52,16 +59,29 @@ test('Verify that user can upload tutorial with local zip file without manifest.
5259
.ok(`${internalLinkName1} is not visible`);
5360
await t.click(await workbenchPage.getAccordionButtonWithName(folder2));
5461
await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName2)).visible)
55-
.ok(`${internalLinkName1} is not visible`);
62+
.ok(`${internalLinkName2} is not visible`);
5663
await t.expect(workbenchPage.scrolledEnablementArea.exists).notOk('enablement area is visible before clicked');
5764
await t.click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1)));
5865
await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked');
66+
67+
// Verify that user can see image in custom tutorials by providing absolute external path in md file
68+
const imageExternal = await workbenchPage.getTutorialImageByAlt(imageExternalPath);
69+
await workbenchPage.waitUntilImageRendered(imageExternal);
70+
const imageExternalHeight = await imageExternal.getStyleProperty('height');
71+
await t.expect(parseInt(imageExternalHeight.replace(/[^\d]/g, ''))).gte(150);
72+
73+
// Verify that user can see image in custom tutorials by providing relative path in md file
74+
const imageRelative = await workbenchPage.getTutorialImageByAlt(imageRelativePath);
75+
await workbenchPage.waitUntilImageRendered(imageRelative);
76+
const imageRelativeHeight = await imageRelative.getStyleProperty('height');
77+
await t.expect(parseInt(imageRelativeHeight.replace(/[^\d]/g, ''))).gte(150);
78+
79+
// Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench
5980
await t.click(workbenchPage.closeEnablementPage);
6081
await t.click(workbenchPage.tutorialLatestDeleteIcon);
6182
await t.expect(workbenchPage.tutorialDeleteButton.visible).ok('Delete popup is not visible');
6283
await t.click(workbenchPage.tutorialDeleteButton);
6384
await t.expect(workbenchPage.tutorialDeleteButton.exists).notOk('Delete popup is still visible');
64-
// Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench
6585
await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists))
6686
.notOk(`${tutorialName} tutorial is not uploaded`);
6787
});

0 commit comments

Comments
 (0)