mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Maps as code] Refactor MapsStorage class to allow custom content management transforms (#224297)
Refactor the [MapsStorage class](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/maps/server/content_management/maps_storage.ts) to stop extending from SOContentStorage. Instead, the MapsStorage class will define its own get, create, update, delete, mSearch, and search methods. Conceptually, this will be very similar to the https://github.com/elastic/kibana/pull/221985. Closes #222586
This commit is contained in:
parent
e5503c7b97
commit
21200b848e
7 changed files with 439 additions and 34 deletions
|
@ -9,7 +9,7 @@ export { LATEST_VERSION, CONTENT_ID } from './constants';
|
|||
|
||||
export type { MapContentType } from './types';
|
||||
|
||||
export type { MapCrudTypes, MapAttributes, MapItem } from './latest';
|
||||
export type { MapCrudTypes, MapAttributes, MapItem, MapsSearchOut } from './latest';
|
||||
|
||||
// Today "v1" === "latest" so the export under MapV1 namespace is not really useful
|
||||
// We leave it as a reference for future version when it will be needed to export/support older types
|
||||
|
|
|
@ -8,3 +8,4 @@
|
|||
import { MapCrudTypes } from './types';
|
||||
export type { MapCrudTypes, MapAttributes } from './types';
|
||||
export type MapItem = MapCrudTypes['Item'];
|
||||
export type MapsSearchOut = MapCrudTypes['SearchOut'];
|
||||
|
|
|
@ -5,29 +5,60 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
|
||||
import { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { CONTENT_ID } from '../../common/content_management';
|
||||
import { StorageContext } from '@kbn/content-management-plugin/server';
|
||||
import { SavedObject, SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
|
||||
import Boom from '@hapi/boom';
|
||||
import { CreateResult, SearchQuery, DeleteResult } from '@kbn/content-management-plugin/common';
|
||||
import type { MapAttributes, MapItem, MapsSearchOut } from '../../common/content_management';
|
||||
import { MAP_SAVED_OBJECT_TYPE } from '../../common';
|
||||
import {
|
||||
MapsSavedObjectAttributes,
|
||||
MapsGetOut,
|
||||
MapsSearchOptions,
|
||||
MapsCreateOptions,
|
||||
MapsCreateOut,
|
||||
MapsUpdateOptions,
|
||||
MapsUpdateOut,
|
||||
} from './schema/v1/types';
|
||||
import { savedObjectToItem, itemToSavedObject } from './schema/v1/transform_utils';
|
||||
import { cmServicesDefinition } from './schema/cm_services';
|
||||
import type { MapCrudTypes } from '../../common/content_management';
|
||||
|
||||
const searchArgsToSOFindOptions = (args: MapCrudTypes['SearchIn']): SavedObjectsFindOptions => {
|
||||
const { query, contentTypeId, options } = args;
|
||||
const savedObjectClientFromRequest = async (ctx: StorageContext) => {
|
||||
if (!ctx.requestHandlerContext) {
|
||||
throw new Error('Storage context.requestHandlerContext missing.');
|
||||
}
|
||||
const { savedObjects } = await ctx.requestHandlerContext.core;
|
||||
return savedObjects.client;
|
||||
};
|
||||
|
||||
const searchArgsToSOFindOptions = (
|
||||
query: SearchQuery,
|
||||
options: MapsSearchOptions
|
||||
): SavedObjectsFindOptions => {
|
||||
const hasReference = query.tags?.included?.map((tagId) => ({
|
||||
type: 'tag',
|
||||
id: tagId,
|
||||
}));
|
||||
|
||||
const hasNoReference = query.tags?.excluded?.map((tagId) => ({
|
||||
type: 'tag',
|
||||
id: tagId,
|
||||
}));
|
||||
|
||||
return {
|
||||
type: contentTypeId,
|
||||
type: MAP_SAVED_OBJECT_TYPE,
|
||||
searchFields: options?.onlyTitle ? ['title'] : ['title^3', 'description'],
|
||||
fields: ['description', 'title'],
|
||||
search: query.text,
|
||||
perPage: query.limit,
|
||||
page: query.cursor ? +query.cursor : undefined,
|
||||
defaultSearchOperator: 'AND',
|
||||
...tagsToFindOptions(query.tags),
|
||||
hasReference,
|
||||
hasNoReference,
|
||||
};
|
||||
};
|
||||
|
||||
export class MapsStorage extends SOContentStorage<MapCrudTypes> {
|
||||
export class MapsStorage {
|
||||
constructor({
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
|
@ -35,20 +66,280 @@ export class MapsStorage extends SOContentStorage<MapCrudTypes> {
|
|||
logger: Logger;
|
||||
throwOnResultValidationError: boolean;
|
||||
}) {
|
||||
super({
|
||||
savedObjectType: CONTENT_ID,
|
||||
cmServicesDefinition,
|
||||
searchArgsToSOFindOptions,
|
||||
enableMSearch: true,
|
||||
allowedSavedObjectAttributes: [
|
||||
'title',
|
||||
'description',
|
||||
'mapStateJSON',
|
||||
'layerListJSON',
|
||||
'uiStateJSON',
|
||||
],
|
||||
logger,
|
||||
throwOnResultValidationError,
|
||||
});
|
||||
this.logger = logger;
|
||||
this.throwOnResultValidationError = throwOnResultValidationError ?? false;
|
||||
}
|
||||
|
||||
private logger: Logger;
|
||||
private throwOnResultValidationError: boolean;
|
||||
|
||||
async get(ctx: StorageContext, id: string): Promise<MapsGetOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
const {
|
||||
saved_object: savedObject,
|
||||
alias_purpose: aliasPurpose,
|
||||
alias_target_id: aliasTargetId,
|
||||
outcome,
|
||||
} = await soClient.resolve<MapsSavedObjectAttributes>(MAP_SAVED_OBJECT_TYPE, id);
|
||||
|
||||
const response = {
|
||||
item: savedObject,
|
||||
meta: { aliasPurpose, aliasTargetId, outcome },
|
||||
};
|
||||
|
||||
const validationError = transforms.get.out.result.validate(response);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
const { value, error: resultError } = transforms.get.out.result.down<MapsGetOut, MapsGetOut>(
|
||||
response,
|
||||
undefined,
|
||||
{ validate: false }
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async bulkGet(): Promise<never> {
|
||||
// Not implemented
|
||||
throw new Error(`[bulkGet] has not been implemented. See MapsStorage class.`);
|
||||
}
|
||||
|
||||
async create(
|
||||
ctx: StorageContext,
|
||||
data: MapAttributes,
|
||||
options: MapsCreateOptions
|
||||
): Promise<MapsCreateOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
|
||||
MapAttributes,
|
||||
MapAttributes
|
||||
>(data);
|
||||
if (dataError) {
|
||||
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
|
||||
}
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
|
||||
MapsCreateOptions,
|
||||
MapsCreateOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
const { attributes: soAttributes, references: soReferences } = itemToSavedObject({
|
||||
attributes: dataToLatest,
|
||||
references: options.references,
|
||||
});
|
||||
|
||||
// Save data in DB
|
||||
const savedObject = await soClient.create<MapsSavedObjectAttributes>(
|
||||
MAP_SAVED_OBJECT_TYPE,
|
||||
soAttributes,
|
||||
{ ...optionsToLatest, references: soReferences }
|
||||
);
|
||||
|
||||
const item = savedObjectToItem(savedObject, false);
|
||||
|
||||
const validationError = transforms.create.out.result.validate({ item });
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.create.out.result.down<CreateResult<MapItem>>(
|
||||
{ item },
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async update(
|
||||
ctx: StorageContext,
|
||||
id: string,
|
||||
data: MapAttributes,
|
||||
options: MapsUpdateOptions
|
||||
): Promise<MapsUpdateOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
// Validate input (data & options) & UP transform them to the latest version
|
||||
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
|
||||
MapAttributes,
|
||||
MapAttributes
|
||||
>(data);
|
||||
if (dataError) {
|
||||
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
|
||||
}
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
|
||||
MapsUpdateOptions,
|
||||
MapsUpdateOptions
|
||||
>(options);
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
const { attributes: soAttributes, references: soReferences } = itemToSavedObject({
|
||||
attributes: dataToLatest,
|
||||
references: options.references,
|
||||
});
|
||||
|
||||
// Save data in DB
|
||||
const partialSavedObject = await soClient.update<MapsSavedObjectAttributes>(
|
||||
MAP_SAVED_OBJECT_TYPE,
|
||||
id,
|
||||
soAttributes,
|
||||
{ ...optionsToLatest, references: soReferences }
|
||||
);
|
||||
|
||||
const item = savedObjectToItem(partialSavedObject, true);
|
||||
|
||||
const validationError = transforms.update.out.result.validate({ item });
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.update.out.result.down<
|
||||
MapsUpdateOut,
|
||||
MapsUpdateOut
|
||||
>(
|
||||
{ item },
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async delete(
|
||||
ctx: StorageContext,
|
||||
id: string,
|
||||
// force is necessary to delete saved objects that exist in multiple namespaces
|
||||
options?: { force: boolean }
|
||||
): Promise<DeleteResult> {
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
await soClient.delete(MAP_SAVED_OBJECT_TYPE, id, { force: options?.force ?? false });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async search(
|
||||
ctx: StorageContext,
|
||||
query: SearchQuery,
|
||||
options: MapsSearchOptions
|
||||
): Promise<MapsSearchOut> {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
const soClient = await savedObjectClientFromRequest(ctx);
|
||||
|
||||
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
|
||||
MapsSearchOptions,
|
||||
MapsSearchOptions
|
||||
>(options);
|
||||
|
||||
if (optionsError) {
|
||||
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
|
||||
}
|
||||
|
||||
const soQuery = searchArgsToSOFindOptions(query, optionsToLatest);
|
||||
const soResponse = await soClient.find<MapsSavedObjectAttributes>(soQuery);
|
||||
const hits = await Promise.all(
|
||||
soResponse.saved_objects
|
||||
.map(async (so) => {
|
||||
const item = savedObjectToItem(so, false);
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item !== null)
|
||||
);
|
||||
|
||||
const response = {
|
||||
hits,
|
||||
pagination: {
|
||||
total: soResponse.total,
|
||||
},
|
||||
};
|
||||
|
||||
const validationError = transforms.search.out.result.validate(response);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { value, error: resultError } = transforms.search.out.result.down<
|
||||
MapsSearchOut,
|
||||
MapsSearchOut
|
||||
>(response, undefined, { validate: false });
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
mSearch = {
|
||||
savedObjectType: MAP_SAVED_OBJECT_TYPE,
|
||||
toItemResult: (
|
||||
ctx: StorageContext,
|
||||
savedObject: SavedObject<MapsSavedObjectAttributes>
|
||||
): MapItem => {
|
||||
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
|
||||
|
||||
const contentItem = savedObjectToItem(savedObject, false);
|
||||
|
||||
const validationError = transforms.mSearch.out.result.validate(contentItem);
|
||||
if (validationError) {
|
||||
if (this.throwOnResultValidationError) {
|
||||
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
|
||||
} else {
|
||||
this.logger.warn(`Invalid response. ${validationError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate DB response and DOWN transform to the request version
|
||||
const { value, error: resultError } = transforms.mSearch.out.result.down<MapItem, MapItem>(
|
||||
contentItem,
|
||||
undefined, // do not override version
|
||||
{ validate: false } // validation is done above
|
||||
);
|
||||
|
||||
if (resultError) {
|
||||
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ import {
|
|||
objectTypeToGetResultSchema,
|
||||
createOptionsSchemas,
|
||||
createResultSchema,
|
||||
updateOptionsSchema,
|
||||
} from '@kbn/content-management-utils';
|
||||
|
||||
const mapAttributesSchema = schema.object(
|
||||
export const mapAttributesSchema = schema.object(
|
||||
{
|
||||
title: schema.string(),
|
||||
description: schema.maybe(schema.nullable(schema.string())),
|
||||
|
@ -24,9 +25,9 @@ const mapAttributesSchema = schema.object(
|
|||
{ unknowns: 'forbid' }
|
||||
);
|
||||
|
||||
const mapSavedObjectSchema = savedObjectSchema(mapAttributesSchema);
|
||||
export const mapSavedObjectSchema = savedObjectSchema(mapAttributesSchema);
|
||||
|
||||
const searchOptionsSchema = schema.maybe(
|
||||
export const searchOptionsSchema = schema.maybe(
|
||||
schema.object(
|
||||
{
|
||||
onlyTitle: schema.maybe(schema.boolean()),
|
||||
|
@ -35,10 +36,31 @@ const searchOptionsSchema = schema.maybe(
|
|||
)
|
||||
);
|
||||
|
||||
const createOptionsSchema = schema.object({
|
||||
export const mapsSearchOptionsSchema = schema.maybe(
|
||||
schema.object(
|
||||
{
|
||||
onlyTitle: schema.maybe(schema.boolean()),
|
||||
},
|
||||
{ unknowns: 'forbid' }
|
||||
)
|
||||
);
|
||||
|
||||
export const createOptionsSchema = schema.object({
|
||||
references: schema.maybe(createOptionsSchemas.references),
|
||||
});
|
||||
|
||||
export const mapsCreateOptionsSchema = schema.object({
|
||||
references: schema.maybe(createOptionsSchemas.references),
|
||||
});
|
||||
|
||||
export const mapsUpdateOptionsSchema = schema.object({
|
||||
references: updateOptionsSchema.references,
|
||||
});
|
||||
|
||||
export const mapsGetResultSchema = objectTypeToGetResultSchema(mapSavedObjectSchema);
|
||||
|
||||
export const mapsCreateResultSchema = createResultSchema(mapSavedObjectSchema);
|
||||
|
||||
// Content management service definition.
|
||||
// We need it for BWC support between different versions of the content
|
||||
export const serviceDefinition: ServicesDefinition = {
|
||||
|
@ -60,14 +82,14 @@ export const serviceDefinition: ServicesDefinition = {
|
|||
},
|
||||
out: {
|
||||
result: {
|
||||
schema: createResultSchema(mapSavedObjectSchema),
|
||||
schema: mapsCreateResultSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
in: {
|
||||
options: {
|
||||
schema: createOptionsSchema, // same as create
|
||||
schema: mapsUpdateOptionsSchema, // Is still the same as create?
|
||||
},
|
||||
data: {
|
||||
schema: mapAttributesSchema,
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
|
||||
import type { MapItem, MapAttributes } from '../../../../common/content_management';
|
||||
import type { MapsCreateOptions, MapsSavedObjectAttributes } from './types';
|
||||
|
||||
type PartialSavedObject<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
|
||||
references: SavedObjectReference[] | undefined;
|
||||
};
|
||||
|
||||
interface PartialMapsItem {
|
||||
attributes: Partial<MapItem['attributes']>;
|
||||
references: SavedObjectReference[] | undefined;
|
||||
}
|
||||
|
||||
export function savedObjectToItem(
|
||||
savedObject: SavedObject<MapsSavedObjectAttributes>,
|
||||
partial: false
|
||||
): MapItem;
|
||||
|
||||
export function savedObjectToItem(
|
||||
savedObject: PartialSavedObject<MapsSavedObjectAttributes>,
|
||||
partial: true
|
||||
): PartialMapsItem;
|
||||
|
||||
// export function savedObjectToItem(
|
||||
// savedObject:
|
||||
// | SavedObject<MapsSavedObjectAttributes>
|
||||
// | PartialSavedObject<MapsSavedObjectAttributes>,
|
||||
// partial: boolean
|
||||
// ): MapItem | PartialMapsItem {
|
||||
// return savedObject;
|
||||
// }
|
||||
|
||||
export function savedObjectToItem(
|
||||
savedObject:
|
||||
| SavedObject<MapsSavedObjectAttributes>
|
||||
| PartialSavedObject<MapsSavedObjectAttributes>,
|
||||
partial: boolean
|
||||
): MapItem | PartialMapsItem {
|
||||
const normalizedAttributes = {
|
||||
...savedObject.attributes,
|
||||
description: savedObject.attributes?.description ?? undefined,
|
||||
mapStateJSON: savedObject.attributes?.mapStateJSON ?? undefined,
|
||||
layerListJSON: savedObject.attributes?.layerListJSON ?? undefined,
|
||||
uiStateJSON: savedObject.attributes?.uiStateJSON ?? undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
...savedObject,
|
||||
attributes: normalizedAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
export function itemToSavedObject(item: {
|
||||
attributes: MapAttributes;
|
||||
references?: MapsCreateOptions['references'];
|
||||
}) {
|
||||
return item as SavedObject<MapsSavedObjectAttributes>;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
mapAttributesSchema,
|
||||
mapsGetResultSchema,
|
||||
mapsCreateOptionsSchema,
|
||||
mapsCreateResultSchema,
|
||||
mapsSearchOptionsSchema,
|
||||
mapsUpdateOptionsSchema,
|
||||
} from './cm_services';
|
||||
|
||||
export type MapsSavedObjectAttributes = TypeOf<typeof mapAttributesSchema>;
|
||||
|
||||
export type MapsCreateOptions = TypeOf<typeof mapsCreateOptionsSchema>;
|
||||
export type MapsUpdateOptions = TypeOf<typeof mapsUpdateOptionsSchema>;
|
||||
export type MapsSearchOptions = TypeOf<typeof mapsSearchOptionsSchema>;
|
||||
|
||||
export type MapsGetOut = TypeOf<typeof mapsGetResultSchema>;
|
||||
export type MapsCreateOut = TypeOf<typeof mapsCreateResultSchema>;
|
||||
export type MapsUpdateOut = TypeOf<typeof mapsCreateResultSchema>;
|
|
@ -48,7 +48,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
|
||||
await listingTable.expectItemsCount('map', 2);
|
||||
const itemNames = await listingTable.getAllItemsNames();
|
||||
expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 2 (tag-3)']);
|
||||
expect(itemNames).to.eql(['map 2 (tag-3)', 'map 3 (tag-1 and tag-3)']);
|
||||
});
|
||||
|
||||
it('allows to filter by multiple tags', async () => {
|
||||
|
@ -56,7 +56,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
|
||||
await listingTable.expectItemsCount('map', 3);
|
||||
const itemNames = await listingTable.getAllItemsNames();
|
||||
expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 1 (tag-2)', 'map 2 (tag-3)']);
|
||||
expect(itemNames).to.eql(['map 1 (tag-2)', 'map 2 (tag-3)', 'map 3 (tag-1 and tag-3)']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue