forked from tsai/budibase
204 changed files with 7490 additions and 2081 deletions
@ -1,4 +1,7 @@ |
|||
const generic = require("./src/cache/generic") |
|||
|
|||
module.exports = { |
|||
user: require("./src/cache/user"), |
|||
app: require("./src/cache/appMetadata"), |
|||
...generic, |
|||
} |
|||
|
|||
@ -0,0 +1,49 @@ |
|||
const redis = require("../redis/authRedis") |
|||
const env = require("../environment") |
|||
const { getTenantId } = require("../context") |
|||
|
|||
exports.CacheKeys = { |
|||
CHECKLIST: "checklist", |
|||
} |
|||
|
|||
exports.TTL = { |
|||
ONE_MINUTE: 600, |
|||
ONE_HOUR: 3600, |
|||
ONE_DAY: 86400, |
|||
} |
|||
|
|||
function generateTenantKey(key) { |
|||
const tenantId = getTenantId() |
|||
return `${key}:${tenantId}` |
|||
} |
|||
|
|||
exports.withCache = async (key, ttl, fetchFn) => { |
|||
key = generateTenantKey(key) |
|||
const client = await redis.getCacheClient() |
|||
const cachedValue = await client.get(key) |
|||
if (cachedValue) { |
|||
return cachedValue |
|||
} |
|||
|
|||
try { |
|||
const fetchedValue = await fetchFn() |
|||
|
|||
if (!env.isTest()) { |
|||
await client.store(key, fetchedValue, ttl) |
|||
} |
|||
return fetchedValue |
|||
} catch (err) { |
|||
console.error("Error fetching before cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
exports.bustCache = async key => { |
|||
const client = await redis.getCacheClient() |
|||
try { |
|||
await client.delete(generateTenantKey(key)) |
|||
} catch (err) { |
|||
console.error("Error busting cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
@ -0,0 +1,650 @@ |
|||
const util = require("util") |
|||
const assert = require("assert") |
|||
const wrapEmitter = require("emitter-listener") |
|||
const async_hooks = require("async_hooks") |
|||
|
|||
const CONTEXTS_SYMBOL = "cls@contexts" |
|||
const ERROR_SYMBOL = "error@context" |
|||
|
|||
const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED |
|||
|
|||
let currentUid = -1 |
|||
|
|||
module.exports = { |
|||
getNamespace: getNamespace, |
|||
createNamespace: createNamespace, |
|||
destroyNamespace: destroyNamespace, |
|||
reset: reset, |
|||
ERROR_SYMBOL: ERROR_SYMBOL, |
|||
} |
|||
|
|||
function Namespace(name) { |
|||
this.name = name |
|||
// changed in 2.7: no default context
|
|||
this.active = null |
|||
this._set = [] |
|||
this.id = null |
|||
this._contexts = new Map() |
|||
this._indent = 0 |
|||
this._hook = null |
|||
} |
|||
|
|||
Namespace.prototype.set = function set(key, value) { |
|||
if (!this.active) { |
|||
throw new Error( |
|||
"No context available. ns.run() or ns.bind() must be called first." |
|||
) |
|||
} |
|||
|
|||
this.active[key] = value |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
indentStr + |
|||
"CONTEXT-SET KEY:" + |
|||
key + |
|||
"=" + |
|||
value + |
|||
" in ns:" + |
|||
this.name + |
|||
" currentUid:" + |
|||
currentUid + |
|||
" active:" + |
|||
util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) |
|||
) |
|||
} |
|||
|
|||
return value |
|||
} |
|||
|
|||
Namespace.prototype.get = function get(key) { |
|||
if (!this.active) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.currentId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}` |
|||
) |
|||
} |
|||
return undefined |
|||
} |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
indentStr + |
|||
"CONTEXT-GETTING KEY:" + |
|||
key + |
|||
"=" + |
|||
this.active[key] + |
|||
" (" + |
|||
this.name + |
|||
") currentUid:" + |
|||
currentUid + |
|||
" active:" + |
|||
util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) |
|||
) |
|||
debug2( |
|||
`${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${ |
|||
this.active[key] |
|||
} currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ |
|||
this._set.length |
|||
} active:${util.inspect(this.active)}` |
|||
) |
|||
} |
|||
return this.active[key] |
|||
} |
|||
|
|||
Namespace.prototype.createContext = function createContext() { |
|||
// Prototype inherit existing context if created a new child context within existing context.
|
|||
let context = Object.create(this.active ? this.active : Object.prototype) |
|||
context._ns_name = this.name |
|||
context.id = currentUid |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-CREATED Context: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ |
|||
this._set.length |
|||
} context:${util.inspect(context, { |
|||
showHidden: true, |
|||
depth: 2, |
|||
colors: true, |
|||
})}` |
|||
) |
|||
} |
|||
|
|||
return context |
|||
} |
|||
|
|||
Namespace.prototype.run = function run(fn) { |
|||
let context = this.createContext() |
|||
this.enter(context) |
|||
|
|||
try { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-RUN BEGIN: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ |
|||
this._set.length |
|||
} context:${util.inspect(context)}` |
|||
) |
|||
} |
|||
fn(context) |
|||
return context |
|||
} catch (exception) { |
|||
if (exception) { |
|||
exception[ERROR_SYMBOL] = context |
|||
} |
|||
throw exception |
|||
} finally { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-RUN END: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ |
|||
this._set.length |
|||
} ${util.inspect(context)}` |
|||
) |
|||
} |
|||
this.exit(context) |
|||
} |
|||
} |
|||
|
|||
Namespace.prototype.runAndReturn = function runAndReturn(fn) { |
|||
let value |
|||
this.run(function (context) { |
|||
value = fn(context) |
|||
}) |
|||
return value |
|||
} |
|||
|
|||
/** |
|||
* Uses global Promise and assumes Promise is cls friendly or wrapped already. |
|||
* @param {function} fn |
|||
* @returns {*} |
|||
*/ |
|||
Namespace.prototype.runPromise = function runPromise(fn) { |
|||
let context = this.createContext() |
|||
this.enter(context) |
|||
|
|||
let promise = fn(context) |
|||
if (!promise || !promise.then || !promise.catch) { |
|||
throw new Error("fn must return a promise.") |
|||
} |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2( |
|||
"CONTEXT-runPromise BEFORE: (" + |
|||
this.name + |
|||
") currentUid:" + |
|||
currentUid + |
|||
" len:" + |
|||
this._set.length + |
|||
" " + |
|||
util.inspect(context) |
|||
) |
|||
} |
|||
|
|||
return promise |
|||
.then(result => { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2( |
|||
"CONTEXT-runPromise AFTER then: (" + |
|||
this.name + |
|||
") currentUid:" + |
|||
currentUid + |
|||
" len:" + |
|||
this._set.length + |
|||
" " + |
|||
util.inspect(context) |
|||
) |
|||
} |
|||
this.exit(context) |
|||
return result |
|||
}) |
|||
.catch(err => { |
|||
err[ERROR_SYMBOL] = context |
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2( |
|||
"CONTEXT-runPromise AFTER catch: (" + |
|||
this.name + |
|||
") currentUid:" + |
|||
currentUid + |
|||
" len:" + |
|||
this._set.length + |
|||
" " + |
|||
util.inspect(context) |
|||
) |
|||
} |
|||
this.exit(context) |
|||
throw err |
|||
}) |
|||
} |
|||
|
|||
Namespace.prototype.bind = function bindFactory(fn, context) { |
|||
if (!context) { |
|||
if (!this.active) { |
|||
context = this.createContext() |
|||
} else { |
|||
context = this.active |
|||
} |
|||
} |
|||
|
|||
let self = this |
|||
return function clsBind() { |
|||
self.enter(context) |
|||
try { |
|||
return fn.apply(this, arguments) |
|||
} catch (exception) { |
|||
if (exception) { |
|||
exception[ERROR_SYMBOL] = context |
|||
} |
|||
throw exception |
|||
} finally { |
|||
self.exit(context) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Namespace.prototype.enter = function enter(context) { |
|||
assert.ok(context, "context must be provided for entering") |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-ENTER: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ |
|||
this._set.length |
|||
} ${util.inspect(context)}` |
|||
) |
|||
} |
|||
|
|||
this._set.push(this.active) |
|||
this.active = context |
|||
} |
|||
|
|||
Namespace.prototype.exit = function exit(context) { |
|||
assert.ok(context, "context must be provided for exiting") |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const asyncHooksCurrentId = async_hooks.executionAsyncId() |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) |
|||
debug2( |
|||
`${indentStr}CONTEXT-EXIT: (${ |
|||
this.name |
|||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ |
|||
this._set.length |
|||
} ${util.inspect(context)}` |
|||
) |
|||
} |
|||
|
|||
// Fast path for most exits that are at the top of the stack
|
|||
if (this.active === context) { |
|||
assert.ok(this._set.length, "can't remove top context") |
|||
this.active = this._set.pop() |
|||
return |
|||
} |
|||
|
|||
// Fast search in the stack using lastIndexOf
|
|||
let index = this._set.lastIndexOf(context) |
|||
|
|||
if (index < 0) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2( |
|||
"??ERROR?? context exiting but not entered - ignoring: " + |
|||
util.inspect(context) |
|||
) |
|||
} |
|||
assert.ok( |
|||
index >= 0, |
|||
"context not currently entered; can't exit. \n" + |
|||
util.inspect(this) + |
|||
"\n" + |
|||
util.inspect(context) |
|||
) |
|||
} else { |
|||
assert.ok(index, "can't remove top context") |
|||
this._set.splice(index, 1) |
|||
} |
|||
} |
|||
|
|||
Namespace.prototype.bindEmitter = function bindEmitter(emitter) { |
|||
assert.ok( |
|||
emitter.on && emitter.addListener && emitter.emit, |
|||
"can only bind real EEs" |
|||
) |
|||
|
|||
let namespace = this |
|||
let thisSymbol = "context@" + this.name |
|||
|
|||
// Capture the context active at the time the emitter is bound.
|
|||
function attach(listener) { |
|||
if (!listener) { |
|||
return |
|||
} |
|||
if (!listener[CONTEXTS_SYMBOL]) { |
|||
listener[CONTEXTS_SYMBOL] = Object.create(null) |
|||
} |
|||
|
|||
listener[CONTEXTS_SYMBOL][thisSymbol] = { |
|||
namespace: namespace, |
|||
context: namespace.active, |
|||
} |
|||
} |
|||
|
|||
// At emit time, bind the listener within the correct context.
|
|||
function bind(unwrapped) { |
|||
if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) { |
|||
return unwrapped |
|||
} |
|||
|
|||
let wrapped = unwrapped |
|||
let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL] |
|||
Object.keys(unwrappedContexts).forEach(function (name) { |
|||
let thunk = unwrappedContexts[name] |
|||
wrapped = thunk.namespace.bind(wrapped, thunk.context) |
|||
}) |
|||
return wrapped |
|||
} |
|||
|
|||
wrapEmitter(emitter, attach, bind) |
|||
} |
|||
|
|||
/** |
|||
* If an error comes out of a namespace, it will have a context attached to it. |
|||
* This function knows how to find it. |
|||
* |
|||
* @param {Error} exception Possibly annotated error. |
|||
*/ |
|||
Namespace.prototype.fromException = function fromException(exception) { |
|||
return exception[ERROR_SYMBOL] |
|||
} |
|||
|
|||
function getNamespace(name) { |
|||
return process.namespaces[name] |
|||
} |
|||
|
|||
function createNamespace(name) { |
|||
assert.ok(name, "namespace must be given a name.") |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
debug2(`NS-CREATING NAMESPACE (${name})`) |
|||
} |
|||
let namespace = new Namespace(name) |
|||
namespace.id = currentUid |
|||
|
|||
const hook = async_hooks.createHook({ |
|||
init(asyncId, type, triggerId, resource) { |
|||
currentUid = async_hooks.executionAsyncId() |
|||
|
|||
//CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec
|
|||
// let initContext = namespace.active;
|
|||
// if(!initContext && triggerId) {
|
|||
// let parentContext = namespace._contexts.get(triggerId);
|
|||
// if (parentContext) {
|
|||
// namespace.active = parentContext;
|
|||
// namespace._contexts.set(currentUid, parentContext);
|
|||
// if (DEBUG_CLS_HOOKED) {
|
|||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|||
// debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|||
// }
|
|||
// } else if (DEBUG_CLS_HOOKED) {
|
|||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|||
// debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|||
// }
|
|||
// }else {
|
|||
// namespace._contexts.set(currentUid, namespace.active);
|
|||
// if (DEBUG_CLS_HOOKED) {
|
|||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
|||
// debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
|||
// }
|
|||
// }
|
|||
if (namespace.active) { |
|||
namespace._contexts.set(asyncId, namespace.active) |
|||
|
|||
if (DEBUG_CLS_HOOKED) { |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} resource:${resource}` |
|||
) |
|||
} |
|||
} else if (currentUid === 0) { |
|||
// CurrentId will be 0 when triggered from C++. Promise events
|
|||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const triggerIdContext = namespace._contexts.get(triggerId) |
|||
if (triggerIdContext) { |
|||
namespace._contexts.set(asyncId, triggerIdContext) |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} resource:${resource}` |
|||
) |
|||
} |
|||
} else if (DEBUG_CLS_HOOKED) { |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} resource:${resource}` |
|||
) |
|||
} |
|||
} |
|||
|
|||
if (DEBUG_CLS_HOOKED && type === "PROMISE") { |
|||
debug2(util.inspect(resource, { showHidden: true })) |
|||
const parentId = resource.parentId |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} resource:${resource}` |
|||
) |
|||
} |
|||
}, |
|||
before(asyncId) { |
|||
currentUid = async_hooks.executionAsyncId() |
|||
let context |
|||
|
|||
/* |
|||
if(currentUid === 0){ |
|||
// CurrentId will be 0 when triggered from C++. Promise events
|
|||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|||
//const triggerId = async_hooks.triggerAsyncId();
|
|||
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
|
|||
}else{ |
|||
context = namespace._contexts.get(currentUid); |
|||
} |
|||
*/ |
|||
|
|||
//HACK to work with promises until they are fixed in node > 8.1.1
|
|||
context = |
|||
namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) |
|||
|
|||
if (context) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} context:${util.inspect(context)}` |
|||
) |
|||
namespace._indent += 2 |
|||
} |
|||
|
|||
namespace.enter(context) |
|||
} else if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} namespace._contexts:${util.inspect(namespace._contexts, { |
|||
showHidden: true, |
|||
depth: 2, |
|||
colors: true, |
|||
})}` |
|||
) |
|||
namespace._indent += 2 |
|||
} |
|||
}, |
|||
after(asyncId) { |
|||
currentUid = async_hooks.executionAsyncId() |
|||
let context // = namespace._contexts.get(currentUid);
|
|||
/* |
|||
if(currentUid === 0){ |
|||
// CurrentId will be 0 when triggered from C++. Promise events
|
|||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
|||
//const triggerId = async_hooks.triggerAsyncId();
|
|||
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
|
|||
}else{ |
|||
context = namespace._contexts.get(currentUid); |
|||
} |
|||
*/ |
|||
//HACK to work with promises until they are fixed in node > 8.1.1
|
|||
context = |
|||
namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) |
|||
|
|||
if (context) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
namespace._indent -= 2 |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} context:${util.inspect(context)}` |
|||
) |
|||
} |
|||
|
|||
namespace.exit(context) |
|||
} else if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
namespace._indent -= 2 |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} context:${util.inspect(context)}` |
|||
) |
|||
} |
|||
}, |
|||
destroy(asyncId) { |
|||
currentUid = async_hooks.executionAsyncId() |
|||
if (DEBUG_CLS_HOOKED) { |
|||
const triggerId = async_hooks.triggerAsyncId() |
|||
const indentStr = " ".repeat( |
|||
namespace._indent < 0 ? 0 : namespace._indent |
|||
) |
|||
debug2( |
|||
`${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect( |
|||
namespace.active, |
|||
{ showHidden: true, depth: 2, colors: true } |
|||
)} context:${util.inspect(namespace._contexts.get(currentUid))}` |
|||
) |
|||
} |
|||
|
|||
namespace._contexts.delete(asyncId) |
|||
}, |
|||
}) |
|||
|
|||
hook.enable() |
|||
namespace._hook = hook |
|||
|
|||
process.namespaces[name] = namespace |
|||
return namespace |
|||
} |
|||
|
|||
function destroyNamespace(name) { |
|||
let namespace = getNamespace(name) |
|||
|
|||
assert.ok(namespace, "can't delete nonexistent namespace! \"" + name + '"') |
|||
assert.ok( |
|||
namespace.id, |
|||
"don't assign to process.namespaces directly! " + util.inspect(namespace) |
|||
) |
|||
|
|||
namespace._hook.disable() |
|||
namespace._contexts = null |
|||
process.namespaces[name] = null |
|||
} |
|||
|
|||
function reset() { |
|||
// must unregister async listeners
|
|||
if (process.namespaces) { |
|||
Object.keys(process.namespaces).forEach(function (name) { |
|||
destroyNamespace(name) |
|||
}) |
|||
} |
|||
process.namespaces = Object.create(null) |
|||
} |
|||
|
|||
process.namespaces = process.namespaces || {} |
|||
|
|||
//const fs = require('fs');
|
|||
function debug2(...args) { |
|||
if (DEBUG_CLS_HOOKED) { |
|||
//fs.writeSync(1, `${util.format(...args)}\n`);
|
|||
process._rawDebug(`${util.format(...args)}`) |
|||
} |
|||
} |
|||
|
|||
/*function getFunctionName(fn) { |
|||
if (!fn) { |
|||
return fn; |
|||
} |
|||
if (typeof fn === 'function') { |
|||
if (fn.name) { |
|||
return fn.name; |
|||
} |
|||
return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1]; |
|||
} else if (fn.constructor && fn.constructor.name) { |
|||
return fn.constructor.name; |
|||
} |
|||
}*/ |
|||
@ -1,84 +1,47 @@ |
|||
const cls = require("cls-hooked") |
|||
const cls = require("../clshooked") |
|||
const { newid } = require("../hashing") |
|||
|
|||
const REQUEST_ID_KEY = "requestId" |
|||
|
|||
class FunctionContext { |
|||
static getMiddleware( |
|||
updateCtxFn = null, |
|||
destroyFn = null, |
|||
contextName = "session" |
|||
) { |
|||
const namespace = this.createNamespace(contextName) |
|||
|
|||
return async function (ctx, next) { |
|||
await new Promise( |
|||
namespace.bind(function (resolve, reject) { |
|||
// store a contextual request ID that can be used anywhere (audit logs)
|
|||
namespace.set(REQUEST_ID_KEY, newid()) |
|||
namespace.bindEmitter(ctx.req) |
|||
namespace.bindEmitter(ctx.res) |
|||
|
|||
if (updateCtxFn) { |
|||
updateCtxFn(ctx) |
|||
} |
|||
next() |
|||
.then(resolve) |
|||
.catch(reject) |
|||
.finally(() => { |
|||
if (destroyFn) { |
|||
return destroyFn(ctx) |
|||
} |
|||
}) |
|||
}) |
|||
) |
|||
} |
|||
const MAIN_CTX = cls.createNamespace("main") |
|||
|
|||
function getContextStorage(namespace) { |
|||
if (namespace && namespace.active) { |
|||
let contextData = namespace.active |
|||
delete contextData.id |
|||
delete contextData._ns_name |
|||
return contextData |
|||
} |
|||
return {} |
|||
} |
|||
|
|||
static run(callback, contextName = "session") { |
|||
const namespace = this.createNamespace(contextName) |
|||
|
|||
return namespace.runAndReturn(callback) |
|||
class FunctionContext { |
|||
static run(callback) { |
|||
return MAIN_CTX.runAndReturn(async () => { |
|||
const namespaceId = newid() |
|||
MAIN_CTX.set(REQUEST_ID_KEY, namespaceId) |
|||
const namespace = cls.createNamespace(namespaceId) |
|||
let response = await namespace.runAndReturn(callback) |
|||
cls.destroyNamespace(namespaceId) |
|||
return response |
|||
}) |
|||
} |
|||
|
|||
static setOnContext(key, value, contextName = "session") { |
|||
const namespace = this.createNamespace(contextName) |
|||
static setOnContext(key, value) { |
|||
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) |
|||
const namespace = cls.getNamespace(namespaceId) |
|||
namespace.set(key, value) |
|||
} |
|||
|
|||
static getContextStorage() { |
|||
if (this._namespace && this._namespace.active) { |
|||
let contextData = this._namespace.active |
|||
delete contextData.id |
|||
delete contextData._ns_name |
|||
return contextData |
|||
} |
|||
|
|||
return {} |
|||
} |
|||
|
|||
static getFromContext(key) { |
|||
const context = this.getContextStorage() |
|||
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) |
|||
const namespace = cls.getNamespace(namespaceId) |
|||
const context = getContextStorage(namespace) |
|||
if (context) { |
|||
return context[key] |
|||
} else { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
static destroyNamespace(name = "session") { |
|||
if (this._namespace) { |
|||
cls.destroyNamespace(name) |
|||
this._namespace = null |
|||
} |
|||
} |
|||
|
|||
static createNamespace(name = "session") { |
|||
if (!this._namespace) { |
|||
this._namespace = cls.createNamespace(name) |
|||
} |
|||
return this._namespace |
|||
} |
|||
} |
|||
|
|||
module.exports = FunctionContext |
|||
|
|||
@ -0,0 +1,194 @@ |
|||
require("../../tests/utilities/dbConfig"); |
|||
const { |
|||
generateAppID, |
|||
getDevelopmentAppID, |
|||
getProdAppID, |
|||
isDevAppID, |
|||
isProdAppID, |
|||
getPlatformUrl, |
|||
getScopedConfig |
|||
} = require("../utils") |
|||
const tenancy = require("../../tenancy"); |
|||
const { Configs, DEFAULT_TENANT_ID } = require("../../constants"); |
|||
const env = require("../../environment") |
|||
|
|||
describe("utils", () => { |
|||
describe("app ID manipulation", () => { |
|||
|
|||
function getID() { |
|||
const appId = generateAppID() |
|||
const split = appId.split("_") |
|||
const uuid = split[split.length - 1] |
|||
const devAppId = `app_dev_${uuid}` |
|||
return { appId, devAppId, split, uuid } |
|||
} |
|||
|
|||
it("should be able to generate a new app ID", () => { |
|||
expect(generateAppID().startsWith("app_")).toEqual(true) |
|||
}) |
|||
|
|||
it("should be able to convert a production app ID to development", () => { |
|||
const { appId, uuid } = getID() |
|||
expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) |
|||
}) |
|||
|
|||
it("should be able to convert a development app ID to development", () => { |
|||
const { devAppId, uuid } = getID() |
|||
expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) |
|||
}) |
|||
|
|||
it("should be able to convert a development ID to a production", () => { |
|||
const { devAppId, uuid } = getID() |
|||
expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) |
|||
}) |
|||
|
|||
it("should be able to convert a production ID to production", () => { |
|||
const { appId, uuid } = getID() |
|||
expect(getProdAppID(appId)).toEqual(`app_${uuid}`) |
|||
}) |
|||
|
|||
it("should be able to confirm dev app ID is development", () => { |
|||
const { devAppId } = getID() |
|||
expect(isDevAppID(devAppId)).toEqual(true) |
|||
}) |
|||
|
|||
it("should be able to confirm prod app ID is not development", () => { |
|||
const { appId } = getID() |
|||
expect(isDevAppID(appId)).toEqual(false) |
|||
}) |
|||
|
|||
it("should be able to confirm prod app ID is prod", () => { |
|||
const { appId } = getID() |
|||
expect(isProdAppID(appId)).toEqual(true) |
|||
}) |
|||
|
|||
it("should be able to confirm dev app ID is not prod", () => { |
|||
const { devAppId } = getID() |
|||
expect(isProdAppID(devAppId)).toEqual(false) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
const DB_URL = "http://dburl.com" |
|||
const DEFAULT_URL = "http://localhost:10000" |
|||
const ENV_URL = "http://env.com" |
|||
|
|||
const setDbPlatformUrl = async () => { |
|||
const db = tenancy.getGlobalDB() |
|||
db.put({ |
|||
_id: "config_settings", |
|||
type: Configs.SETTINGS, |
|||
config: { |
|||
platformUrl: DB_URL |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const clearSettingsConfig = async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
const db = tenancy.getGlobalDB() |
|||
try { |
|||
const config = await db.get("config_settings") |
|||
await db.remove("config_settings", config._rev) |
|||
} catch (e) { |
|||
if (e.status !== 404) { |
|||
throw e |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
describe("getPlatformUrl", () => { |
|||
describe("self host", () => { |
|||
|
|||
beforeEach(async () => { |
|||
env._set("SELF_HOST", 1) |
|||
await clearSettingsConfig() |
|||
}) |
|||
|
|||
it("gets the default url", async () => { |
|||
await tenancy.doInTenant(null, async () => { |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(DEFAULT_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("gets the platform url from the environment", async () => { |
|||
await tenancy.doInTenant(null, async () => { |
|||
env._set("PLATFORM_URL", ENV_URL) |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(ENV_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("gets the platform url from the database", async () => { |
|||
await tenancy.doInTenant(null, async () => { |
|||
await setDbPlatformUrl() |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(DB_URL) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
|
|||
describe("cloud", () => { |
|||
const TENANT_AWARE_URL = "http://default.env.com" |
|||
|
|||
beforeEach(async () => { |
|||
env._set("SELF_HOSTED", 0) |
|||
env._set("MULTI_TENANCY", 1) |
|||
env._set("PLATFORM_URL", ENV_URL) |
|||
await clearSettingsConfig() |
|||
}) |
|||
|
|||
it("gets the platform url from the environment without tenancy", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
const url = await getPlatformUrl({ tenantAware: false }) |
|||
expect(url).toBe(ENV_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("gets the platform url from the environment with tenancy", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(TENANT_AWARE_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("never gets the platform url from the database", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
await setDbPlatformUrl() |
|||
const url = await getPlatformUrl() |
|||
expect(url).toBe(TENANT_AWARE_URL) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
describe("getScopedConfig", () => { |
|||
describe("settings config", () => { |
|||
|
|||
beforeEach(async () => { |
|||
env._set("SELF_HOSTED", 1) |
|||
env._set("PLATFORM_URL", "") |
|||
await clearSettingsConfig() |
|||
}) |
|||
|
|||
it("returns the platform url with an existing config", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
await setDbPlatformUrl() |
|||
const db = tenancy.getGlobalDB() |
|||
const config = await getScopedConfig(db, { type: Configs.SETTINGS }) |
|||
expect(config.platformUrl).toBe(DB_URL) |
|||
}) |
|||
}) |
|||
|
|||
it("returns the platform url without an existing config", async () => { |
|||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
|||
const db = tenancy.getGlobalDB() |
|||
const config = await getScopedConfig(db, { type: Configs.SETTINGS }) |
|||
expect(config.platformUrl).toBe(DEFAULT_URL) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,68 @@ |
|||
<script> |
|||
import "@spectrum-css/fieldgroup/dist/index-vars.css" |
|||
import "@spectrum-css/radio/dist/index-vars.css" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let direction = "vertical" |
|||
export let value = [] |
|||
export let options = [] |
|||
export let error = null |
|||
export let disabled = false |
|||
export let getOptionLabel = option => option |
|||
export let getOptionValue = option => option |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const onChange = e => { |
|||
let tempValue = value |
|||
let isChecked = e.target.checked |
|||
if (!tempValue.includes(e.target.value) && isChecked) { |
|||
tempValue.push(e.target.value) |
|||
} |
|||
value = tempValue |
|||
dispatch( |
|||
"change", |
|||
tempValue.filter(val => val !== e.target.value || isChecked) |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}> |
|||
{#if options && Array.isArray(options)} |
|||
{#each options as option} |
|||
<div |
|||
title={getOptionLabel(option)} |
|||
class="spectrum-Checkbox spectrum-FieldGroup-item" |
|||
class:is-invalid={!!error} |
|||
> |
|||
<label |
|||
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item" |
|||
> |
|||
<input |
|||
on:change={onChange} |
|||
value={getOptionValue(option)} |
|||
type="checkbox" |
|||
class="spectrum-Checkbox-input" |
|||
{disabled} |
|||
checked={value.includes(getOptionValue(option))} |
|||
/> |
|||
<span class="spectrum-Checkbox-box"> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Checkbox-checkmark" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Checkmark100" /> |
|||
</svg> |
|||
</span> |
|||
<span class="spectrum-Checkbox-label">{getOptionLabel(option)}</span> |
|||
</label> |
|||
</div> |
|||
{/each} |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.spectrum-Checkbox-input { |
|||
opacity: 0; |
|||
} |
|||
</style> |
|||
@ -1,11 +1,19 @@ |
|||
{ |
|||
"baseUrl": "http://localhost:4100", |
|||
"video": false, |
|||
"video": true, |
|||
"projectId": "bmbemn", |
|||
"reporter": "cypress-multi-reporters", |
|||
"reporterOptions": { |
|||
"configFile": "reporterConfig.json" |
|||
}, |
|||
"env": { |
|||
"PORT": "4100", |
|||
"WORKER_PORT": "4200", |
|||
"JWT_SECRET": "test", |
|||
"HOST_IP": "" |
|||
}, |
|||
"retries": { |
|||
"runMode": 2, |
|||
"openMode": 0 |
|||
} |
|||
} |
|||
|
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,346 @@ |
|||
import filterTests from "../support/filterTests" |
|||
import clientPackage from "@budibase/client/package.json" |
|||
|
|||
filterTests(['all'], () => { |
|||
context("Application Overview screen", () => { |
|||
before(() => { |
|||
cy.login() |
|||
cy.createTestApp() |
|||
}) |
|||
|
|||
it("Should be accessible from the applications list", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
|
|||
cy.get(".appTable .title").eq(0) |
|||
.invoke('attr', 'data-cy') |
|||
.then(($dataCy) => { |
|||
const dataCy = $dataCy; |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.location().should((loc) => { |
|||
expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy) |
|||
}) |
|||
}) |
|||
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
|
|||
cy.get(".appTable .title").eq(0) |
|||
.invoke('attr', 'data-cy') |
|||
.then(($dataCy) => { |
|||
const dataCy = $dataCy; |
|||
cy.get(".appTable .app-row-actions button").contains("View").click({force: true}) |
|||
|
|||
cy.location().should((loc) => { |
|||
expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy) |
|||
}) |
|||
}) |
|||
|
|||
}) |
|||
|
|||
// Find a more suitable place for this.
|
|||
it("Should allow unlocking in the app list", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
|
|||
cy.get(".appTable .lock-status").eq(0).contains("Locked by you").click() |
|||
|
|||
cy.unlockApp({ owned : true }) |
|||
|
|||
cy.get(".appTable").should("exist") |
|||
cy.get(".lock-status").should('not.be.visible') |
|||
}) |
|||
|
|||
it("Should allow unlocking in the app overview screen", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
|
|||
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) |
|||
cy.wait(1000) |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
|
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".lock-status").eq(0).contains("Locked by you").click() |
|||
|
|||
cy.unlockApp({ owned : true }) |
|||
|
|||
cy.get(".lock-status").should("not.be.visible") |
|||
}) |
|||
|
|||
it("Should reflect the deploy state of an app that hasn't been published.", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
|
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("be.disabled") |
|||
|
|||
cy.get(".spectrum-Tabs-item.is-selected").contains("Overview") |
|||
cy.get(".overview-tab").should("be.visible") |
|||
|
|||
cy.get(".overview-tab [data-cy='app-status']").within(() => { |
|||
cy.get(".status-display").contains("Unpublished") |
|||
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") |
|||
cy.get(".status-text").contains("-") |
|||
}) |
|||
}) |
|||
|
|||
it("Should reflect the app deployment state", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) |
|||
|
|||
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true }) |
|||
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible") |
|||
.within(() => { |
|||
cy.get(".spectrum-Button").contains("Publish").click({ force : true }) |
|||
cy.wait(1000) |
|||
}); |
|||
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("not.be.disabled") |
|||
|
|||
cy.get(".overview-tab [data-cy='app-status']").within(() => { |
|||
cy.get(".status-display").contains("Published") |
|||
cy.get(".status-display .icon svg[aria-label='GlobeCheck']").should("exist") |
|||
cy.get(".status-text").contains("Last published a few seconds ago") |
|||
}) |
|||
}) |
|||
|
|||
it("Should reflect an application that has been unpublished", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) |
|||
|
|||
cy.get(".deployment-top-nav svg[aria-label='Globe']") |
|||
.click({ force: true }) |
|||
|
|||
cy.get("[data-cy='publish-popover-menu']").should("be.visible") |
|||
cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']") |
|||
.click({ force : true }) |
|||
|
|||
cy.get("[data-cy='unpublish-modal']").should("be.visible") |
|||
.within(() => { |
|||
cy.get(".confirm-wrap button").click({ force: true } |
|||
)}) |
|||
cy.wait(1000) |
|||
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".overview-tab [data-cy='app-status']").within(() => { |
|||
cy.get(".status-display").contains("Unpublished") |
|||
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") |
|||
cy.get(".status-text").contains("Last published a few seconds ago") |
|||
}) |
|||
}) |
|||
|
|||
it("Should allow the editing of the application icon", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
|
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".app-logo .edit-hover").should("exist").invoke("show").click() |
|||
|
|||
cy.customiseAppIcon() |
|||
|
|||
cy.get(".app-logo") |
|||
.within(() => { |
|||
cy.get('[aria-label]').eq(0).children() |
|||
.should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps') |
|||
cy.get(".app-icon") |
|||
.should('have.attr', 'style').and('contains', 'color') |
|||
}) |
|||
}) |
|||
|
|||
it("Should reflect the last time the application was edited", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".header-right button").contains("Edit").click({ force: true }); |
|||
|
|||
cy.navigateToFrontend() |
|||
|
|||
cy.addComponent("Elements", "Headline").then(componentId => { |
|||
cy.getComponent(componentId).should("exist") |
|||
}) |
|||
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".overview-tab [data-cy='edited-by']").within(() => { |
|||
cy.get(".editor-name").contains("You") |
|||
cy.get(".last-edit-text").contains("Last edited a few seconds ago") |
|||
}) |
|||
}); |
|||
|
|||
it("Should reflect application version is up-to-date", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".overview-tab [data-cy='app-version']").within(() => { |
|||
cy.get(".version-status").contains("You're running the latest!") |
|||
}) |
|||
}); |
|||
|
|||
it("Should navigate to the settings tab when clicking the App Version card header", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".spectrum-Tabs-item.is-selected").contains("Overview") |
|||
cy.get(".overview-tab").should("be.visible") |
|||
|
|||
cy.get(".overview-tab [data-cy='app-version'] .dash-card-header").click({ force : true }) |
|||
|
|||
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") |
|||
cy.get(".settings-tab").should("be.visible") |
|||
cy.get(".overview-tab").should("not.exist") |
|||
|
|||
}); |
|||
|
|||
it("Should allow the upgrading of an application, if available.", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
cy.wait(500) |
|||
|
|||
cy.location().then(loc => { |
|||
const params = loc.pathname.split("/") |
|||
const appId = params[params.length - 1] |
|||
cy.log(appId) |
|||
//Downgrade the app for the test
|
|||
cy.alterAppVersion(appId, "0.0.1-alpha.0") |
|||
.then(()=>{ |
|||
cy.reload() |
|||
cy.wait(1000) |
|||
cy.log("Current deployment version: " + clientPackage.version) |
|||
|
|||
cy.get(".version-status a").contains("Update").click() |
|||
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") |
|||
|
|||
cy.get(".version-section .page-action button").contains("Update").click({ force: true }) |
|||
|
|||
cy.intercept('POST', '**/applications/**/client/update').as('updateVersion') |
|||
cy.get(".spectrum-Modal.is-open button").contains("Update").click({ force: true }) |
|||
|
|||
cy.wait("@updateVersion") |
|||
.its('response.statusCode').should('eq', 200) |
|||
.then(() => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".spectrum-Tabs-item").contains("Overview").click({ force: true }) |
|||
cy.get(".overview-tab [data-cy='app-version']").within(() => { |
|||
cy.get(".spectrum-Heading").contains(clientPackage.version) |
|||
cy.get(".version-status").contains("You're running the latest!") |
|||
}) |
|||
}) |
|||
}) |
|||
}); |
|||
|
|||
}) |
|||
|
|||
it("Should allow editing of the app details.", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".spectrum-Tabs-item").contains("Settings").click() |
|||
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") |
|||
cy.get(".settings-tab").should("be.visible") |
|||
|
|||
cy.get(".details-section .page-action button").contains("Edit").click({ force: true }) |
|||
cy.updateAppName("sample name") |
|||
|
|||
//publish and check its disabled
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) |
|||
|
|||
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true }) |
|||
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible") |
|||
.within(() => { |
|||
cy.get(".spectrum-Button").contains("Publish").click({ force : true }) |
|||
cy.wait(1000) |
|||
}); |
|||
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
cy.get(".spectrum-Tabs-item").contains("Settings").click() |
|||
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") |
|||
|
|||
cy.get(".details-section .page-action .spectrum-Button").scrollIntoView() |
|||
cy.wait(1000) |
|||
cy.get(".details-section .page-action .spectrum-Button").should("be.disabled") |
|||
|
|||
}) |
|||
|
|||
it("Should allow copying of the published application Id", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .app-row-actions").eq(0) |
|||
.within(() => { |
|||
cy.get(".spectrum-Button").contains("Edit").click({ force: true }) |
|||
}) |
|||
|
|||
cy.publishApp("sample-name") |
|||
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".app-overview-actions-icon > .icon").click({ force : true }) |
|||
|
|||
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { |
|||
cy.get(".spectrum-Menu-item").contains("Copy App ID").click({ force: true }) |
|||
}) |
|||
|
|||
cy.get(".spectrum-Toast-content").contains("App ID copied to clipboard.").should("be.visible") |
|||
}) |
|||
|
|||
it("Should allow unpublishing of the application", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".app-overview-actions-icon > .icon").click({ force : true }) |
|||
|
|||
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { |
|||
cy.get(".spectrum-Menu-item").contains("Unpublish").click({ force: true }) |
|||
cy.wait(500) |
|||
}) |
|||
|
|||
cy.get("[data-cy='unpublish-modal']").should("be.visible") |
|||
.within(() => { |
|||
cy.get(".confirm-wrap button").click({ force: true } |
|||
)}) |
|||
|
|||
cy.get(".overview-tab [data-cy='app-status']").within(() => { |
|||
cy.get(".status-display").contains("Unpublished") |
|||
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") |
|||
}) |
|||
}) |
|||
|
|||
it("Should allow deleting of the application", () => { |
|||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
|||
cy.get(".appTable .name").eq(0).click() |
|||
|
|||
cy.get(".app-overview-actions-icon > .icon").click({ force : true }) |
|||
|
|||
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { |
|||
cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true }) |
|||
cy.wait(500) |
|||
}) |
|||
|
|||
//The test application was renamed earlier in the spec
|
|||
cy.get(".spectrum-Dialog-grid").within(() => { |
|||
cy.get("input").type("sample name") |
|||
cy.get(".spectrum-Button--warning").click() |
|||
}) |
|||
|
|||
cy.location().should((loc) => { |
|||
expect(loc.pathname).to.eq('/builder/portal/apps') |
|||
}) |
|||
|
|||
cy.get(".appTable").should("not.exist") |
|||
|
|||
cy.get(".welcome .container h1").contains("Let's create your first app!") |
|||
}) |
|||
|
|||
after(() => { |
|||
cy.deleteAllApps() |
|||
}) |
|||
|
|||
}) |
|||
}) |
|||
@ -0,0 +1,222 @@ |
|||
import filterTests from "../../../support/filterTests" |
|||
|
|||
filterTests(["all"], () => { |
|||
context("Job Application Tracker Template Functionality", () => { |
|||
const templateName = "Job Application Tracker" |
|||
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') |
|||
|
|||
before(() => { |
|||
cy.login() |
|||
cy.deleteApp(templateName) |
|||
// Template navigation
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { |
|||
onBeforeLoad(win) { |
|||
cy.stub(win, 'open') |
|||
} |
|||
}) |
|||
cy.wait(2000) |
|||
}) |
|||
|
|||
it("should create and publish app with Job Application Tracker template", () => { |
|||
// Select Job Application Tracker template
|
|||
cy.get(".template-thumbnail-text") |
|||
.contains(templateName).parentsUntil(".template-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Use template").click({ force: true }) |
|||
}) |
|||
|
|||
// Confirm URL matches template name
|
|||
const appUrl = cy.get(".app-server") |
|||
appUrl.invoke('text').then(appUrlText => { |
|||
expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
}) |
|||
|
|||
// Create App
|
|||
cy.get(".spectrum-Dialog-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Create app").click({ force: true }) |
|||
}) |
|||
|
|||
// Publish App & Verify it opened
|
|||
cy.wait(2000) // Wait for app to generate
|
|||
cy.publishApp(true) |
|||
cy.window().its('open').should('be.calledOnce') |
|||
}) |
|||
|
|||
it("should add active/inactive vacancies", () => { |
|||
// Visit published app
|
|||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
|
|||
// loop for active/inactive vacancies
|
|||
for (let i = 0; i < 2; i++) { |
|||
// Vacancies section
|
|||
cy.get(".links").contains("Vacancies").click({ force: true }) |
|||
cy.get(".spectrum-Button").contains("Create New").click() |
|||
|
|||
// Add inactive vacancy
|
|||
// Title
|
|||
cy.get('[data-name="Title"]').within(() => { |
|||
cy.get(".spectrum-Textfield").type("Tester") |
|||
}) |
|||
|
|||
// Closing Date
|
|||
cy.get('[data-name="Closing date"]').within(() => { |
|||
cy.get('[aria-label=Calendar]').click({ force: true }) |
|||
}) |
|||
cy.get("[aria-current=date]").click() |
|||
|
|||
// Department
|
|||
cy.get('[data-name="Department"]').within(() => { |
|||
cy.get(".spectrum-Picker-label").click() |
|||
}) |
|||
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
|||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() |
|||
}) |
|||
|
|||
// Employment Type
|
|||
cy.get('[data-name="Employment type"]').within(() => { |
|||
cy.get(".spectrum-Picker-label").click() |
|||
}) |
|||
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
|||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() |
|||
}) |
|||
|
|||
// Salary
|
|||
cy.get('[data-name="Salary ($)"]').within(() => { |
|||
cy.get(".spectrum-Textfield").type(40000) |
|||
}) |
|||
|
|||
// Description
|
|||
cy.get('[data-name="Description"]').within(() => { |
|||
cy.get(".spectrum-Textfield").type("description") |
|||
}) |
|||
|
|||
// Responsibilities
|
|||
cy.get('[data-name="Responsibilities"]').within(() => { |
|||
cy.get(".spectrum-Textfield").type("Responsibilities") |
|||
}) |
|||
|
|||
// Requirements
|
|||
cy.get('[data-name="Requirements"]').within(() => { |
|||
cy.get(".spectrum-Textfield").type("Requirements") |
|||
}) |
|||
|
|||
// Hiring manager
|
|||
cy.get('[data-name="Hiring manager"]').within(() => { |
|||
cy.get(".spectrum-Picker-label").click() |
|||
}) |
|||
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
|||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() |
|||
}) |
|||
|
|||
// Active
|
|||
if (i == 0) { |
|||
cy.get('[data-name="Active"]').within(() => { |
|||
cy.get(".spectrum-Checkbox-box").click({ force: true }) |
|||
}) |
|||
} |
|||
|
|||
// Location
|
|||
cy.get('[data-name="Location"]').within(() => { |
|||
cy.get(".spectrum-Picker-label").click() |
|||
}) |
|||
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
|||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() |
|||
}) |
|||
|
|||
// Save vacancy
|
|||
cy.get(".spectrum-Button").contains("Save").click({ force: true }) |
|||
cy.wait(1000) |
|||
|
|||
// Check table was updated
|
|||
cy.get('[data-name="Vacancies Table"]').eq(i).should('contain', 'Tester') |
|||
} |
|||
}) |
|||
|
|||
xit("should filter applications by stage", () => { |
|||
// Visit published app
|
|||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
cy.wait(1000) |
|||
|
|||
// Applications section
|
|||
cy.get(".links").contains("Applications").click({ force: true }) |
|||
cy.wait(1000) |
|||
|
|||
// Filter by stage - Confirm table updates
|
|||
cy.get(".spectrum-Picker").contains("Filter by stage").click({ force: true }) |
|||
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
|||
for (let i = 1; i < len; i++) { |
|||
cy.get(".spectrum-Menu-item").eq(i).click() |
|||
const stage = cy.get(".spectrum-Picker-label") |
|||
stage.invoke('text').then(stageText => { |
|||
if (stageText == "1st interview") { |
|||
cy.get(".placeholder").should('contain', 'No rows found') |
|||
} |
|||
else { |
|||
cy.get(".spectrum-Table-row").should('contain', stageText) |
|||
} |
|||
cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) |
|||
}) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
xit("should edit an application", () => { |
|||
// Switch application from not hired to hired
|
|||
// Visit published app
|
|||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
cy.wait(1000) |
|||
|
|||
// Not Hired section
|
|||
cy.get(".links").contains("Not hired").click({ force: true }) |
|||
cy.wait(500) |
|||
|
|||
// View application
|
|||
cy.get(".spectrum-Table").within(() => { |
|||
cy.get(".spectrum-Button").contains("View").click({ force: true }) |
|||
cy.wait(500) |
|||
}) |
|||
|
|||
// Update value for 'Staged'
|
|||
cy.get('[data-name="Stage"]').within(() => { |
|||
cy.get(".spectrum-Picker-label").click() |
|||
}) |
|||
cy.get(".spectrum-Menu").within(() => { |
|||
cy.get(".spectrum-Menu-item").contains("Hired").click() |
|||
}) |
|||
|
|||
// Save application
|
|||
cy.get(".spectrum-Button").contains("Save").click({ force: true }) |
|||
cy.wait(500) |
|||
|
|||
// Hired section
|
|||
cy.get(".links").contains("Hired").click({ force: true }) |
|||
cy.wait(500) |
|||
|
|||
// Verify Table size - Total rows = 2
|
|||
cy.get(".spectrum-Table").find(".spectrum-Table-row").its('length').then((len => { |
|||
expect(len).to.eq(2) |
|||
})) |
|||
}) |
|||
|
|||
xit("should delete an application", () => { |
|||
// Visit published app
|
|||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
cy.wait(1000) |
|||
|
|||
// Hired section
|
|||
cy.get(".links").contains("Hired").click({ force: true }) |
|||
cy.wait(500) |
|||
|
|||
// View first application
|
|||
cy.get(".spectrum-Table-row").eq(0).within(() => { |
|||
cy.get(".spectrum-Button").contains("View").click({ force: true }) |
|||
cy.wait(500) |
|||
}) |
|||
|
|||
// Delete application
|
|||
cy.get(".spectrum-Button").contains("Delete").click({ force: true }) |
|||
cy.get(".spectrum-Dialog-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Confirm").click() |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,72 @@ |
|||
import filterTests from "../../../support/filterTests" |
|||
|
|||
filterTests(["all"], () => { |
|||
context("IT Ticketing System Template Functionality", () => { |
|||
const templateName = "IT Ticketing System" |
|||
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') |
|||
|
|||
before(() => { |
|||
cy.login() |
|||
cy.deleteApp(templateName) |
|||
// Template navigation
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { |
|||
onBeforeLoad(win) { |
|||
cy.stub(win, 'open') |
|||
} |
|||
}) |
|||
cy.wait(2000) |
|||
}) |
|||
|
|||
it("should create and publish app with IT Ticketing System template", () => { |
|||
// Select IT Ticketing System template
|
|||
cy.get(".template-thumbnail-text") |
|||
.contains(templateName).parentsUntil(".template-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Use template").click({ force: true }) |
|||
}) |
|||
|
|||
// Confirm URL matches template name
|
|||
const appUrl = cy.get(".app-server") |
|||
appUrl.invoke('text').then(appUrlText => { |
|||
expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
}) |
|||
|
|||
// Create App
|
|||
cy.get(".spectrum-Dialog-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Create app").click({ force: true }) |
|||
}) |
|||
|
|||
// Publish App & Verify it opened
|
|||
cy.wait(2000) // Wait for app to generate
|
|||
cy.publishApp(true) |
|||
cy.window().its('open').should('be.calledOnce') |
|||
}) |
|||
|
|||
xit("should filter tickets by status", () => { |
|||
// Visit published app
|
|||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
cy.wait(1000) |
|||
|
|||
// Tickets section
|
|||
cy.get(".links").contains("Tickets").click({ force: true }) |
|||
cy.wait(1000) |
|||
|
|||
// Filter by stage - Confirm table updates
|
|||
cy.get(".spectrum-Picker").contains("Filter by status").click({ force: true }) |
|||
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
|||
for (let i = 1; i < len; i++) { |
|||
cy.get(".spectrum-Menu-item").eq(i).click() |
|||
const stage = cy.get(".spectrum-Picker-label") |
|||
stage.invoke('text').then(stageText => { |
|||
if (stageText == "In progress" || stageText == "On hold" || stageText == "Triaged") { |
|||
cy.get(".placeholder").should('contain', 'No rows found') |
|||
} |
|||
else { |
|||
cy.get(".spectrum-Table-row").should('contain', stageText) |
|||
} |
|||
cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) |
|||
}) |
|||
} |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,44 @@ |
|||
import filterTests from "../../../support/filterTests" |
|||
|
|||
filterTests(["all"], () => { |
|||
context("Lead Generation Form Template Functionality", () => { |
|||
const templateName = "Lead Generation Form" |
|||
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') |
|||
|
|||
before(() => { |
|||
cy.login() |
|||
cy.deleteApp(templateName) |
|||
// Template navigation
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { |
|||
onBeforeLoad(win) { |
|||
cy.stub(win, 'open') |
|||
} |
|||
}) |
|||
cy.wait(2000) |
|||
}) |
|||
|
|||
it("should create and publish app with Lead Generation Form template", () => { |
|||
// Select Lead Generation Form template
|
|||
cy.get(".template-thumbnail-text") |
|||
.contains(templateName).parentsUntil(".template-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Use template").click({ force: true }) |
|||
}) |
|||
|
|||
// Confirm URL matches template name
|
|||
const appUrl = cy.get(".app-server") |
|||
appUrl.invoke('text').then(appUrlText => { |
|||
expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
}) |
|||
|
|||
// Create App
|
|||
cy.get(".spectrum-Dialog-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Create app").click({ force: true }) |
|||
}) |
|||
|
|||
// Publish App & Verify it opened
|
|||
cy.wait(2000) // Wait for app to generate
|
|||
cy.publishApp(true) |
|||
cy.window().its('open').should('be.calledOnce') |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,26 @@ |
|||
// createApp test
|
|||
export const CREATE_APP_BUTTON = '[data-cy="create-app-btn"]' |
|||
export const TEMPLATE_CATEGORY_FILTER = ".template-category-filters" |
|||
export const TEMPLATE_CATEGORY = ".template-categories" |
|||
export const APP_TABLE = ".appTable" |
|||
export const SPECTRUM_BUTTON_TEMPLATE = ".spectrum-Button" |
|||
export const TEMPLATE_CATEGORY_ACTIONGROUP = ".template-category" |
|||
export const TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON = |
|||
".template-category-filters .spectrum-ActionButton" |
|||
export const SPECTRUM_MODAL = ".spectrum-Modal" |
|||
export const APP_NAME_INPUT = "input" // we need to update this with atribute cy-data;
|
|||
export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup" |
|||
export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input" |
|||
|
|||
//AddMultiOptionDatatype test
|
|||
export const CATEGORY_DATA = '[data-cy="category-Data"]' |
|||
export const COMPONENT_DATA_PROVIDER = '[data-cy="component-Data Provider"]' |
|||
export const DATASOURCE_PROP_CONTROL = '[data-cy="dataSource-prop-control"]' |
|||
export const DROPDOWN = ".dropdown" |
|||
export const SPECTRUM_Picker_LABEL = ".spectrum-Picker-label" |
|||
export const DATASOURCE_FIELD_CONTROL = '[data-cy="field-prop-control"]' |
|||
export const OPTION_TYPE_PROP_CONTROL = '[data-cy="optionsType-prop-control' |
|||
|
|||
//AddRadioButtons
|
|||
export const SPECTRUM_POPOVER = ".spectrum-Popover" |
|||
export const OPTION_SOURCE_PROP_CONROL = '[data-cy="optionsSource-prop-control' |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"reporterEnabled": "mochawesome", |
|||
"mochawesomeReporterOptions": { |
|||
"reportDir": "cypress/reports", |
|||
"quiet": true, |
|||
"overwrite": false, |
|||
"html": false, |
|||
"json": true |
|||
} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
const fetch = require("node-fetch") |
|||
const path = require("path") |
|||
const fs = require("fs") |
|||
|
|||
const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL |
|||
const OUTCOME = process.env.CYPRESS_OUTCOME |
|||
const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL |
|||
const GIT_SHA = process.env.GITHUB_SHA |
|||
const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL |
|||
|
|||
async function generateReport() { |
|||
// read the report file
|
|||
const REPORT_PATH = path.resolve( |
|||
__dirname, |
|||
"..", |
|||
"cypress", |
|||
"reports", |
|||
"testReport.json" |
|||
) |
|||
const report = fs.readFileSync(REPORT_PATH, "utf-8") |
|||
return JSON.parse(report) |
|||
} |
|||
|
|||
async function discordCypressResultsNotification(report) { |
|||
const { |
|||
suites, |
|||
tests, |
|||
passes, |
|||
pending, |
|||
failures, |
|||
duration, |
|||
passPercent, |
|||
skipped, |
|||
} = report.stats |
|||
|
|||
const options = { |
|||
method: "POST", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
Accept: "application/json", |
|||
}, |
|||
body: JSON.stringify({ |
|||
content: `**Nightly Tests Status**: ${OUTCOME}`, |
|||
embeds: [ |
|||
{ |
|||
title: "Budi QA Bot", |
|||
description: `Nightly Tests`, |
|||
url: GITHUB_ACTIONS_RUN_URL, |
|||
color: OUTCOME === "success" ? 3066993 : 15548997, |
|||
timestamp: new Date(), |
|||
footer: { |
|||
icon_url: "http://bbui.budibase.com/budibase-logo.png", |
|||
text: "Budibase QA Bot", |
|||
}, |
|||
thumbnail: { |
|||
url: "http://bbui.budibase.com/budibase-logo.png", |
|||
}, |
|||
author: { |
|||
name: "Budibase QA Bot", |
|||
url: "https://discordapp.com", |
|||
icon_url: "http://bbui.budibase.com/budibase-logo.png", |
|||
}, |
|||
fields: [ |
|||
{ |
|||
name: "Commit", |
|||
value: `https://github.com/Budibase/budibase/commit/${GIT_SHA}`, |
|||
}, |
|||
{ |
|||
name: "Cypress Dashboard URL", |
|||
value: DASHBOARD_URL || "None Supplied", |
|||
}, |
|||
{ |
|||
name: "Github Actions Run URL", |
|||
value: GITHUB_ACTIONS_RUN_URL || "None Supplied", |
|||
}, |
|||
{ |
|||
name: "Test Suites", |
|||
value: suites, |
|||
}, |
|||
{ |
|||
name: "Tests", |
|||
value: tests, |
|||
}, |
|||
{ |
|||
name: "Passed", |
|||
value: passes, |
|||
}, |
|||
{ |
|||
name: "Pending", |
|||
value: pending, |
|||
}, |
|||
{ |
|||
name: "Skipped", |
|||
value: skipped, |
|||
}, |
|||
{ |
|||
name: "Failures", |
|||
value: failures, |
|||
}, |
|||
{ |
|||
name: "Duration", |
|||
value: `${duration / 1000} Seconds`, |
|||
}, |
|||
{ |
|||
name: "Pass Percentage", |
|||
value: Math.floor(passPercent), |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}), |
|||
} |
|||
const response = await fetch(WEBHOOK_URL, options) |
|||
|
|||
if (response.status >= 400) { |
|||
const text = await response.text() |
|||
console.error( |
|||
`Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}` |
|||
) |
|||
} |
|||
} |
|||
|
|||
async function run() { |
|||
const report = await generateReport() |
|||
await discordCypressResultsNotification(report) |
|||
} |
|||
|
|||
run() |
|||
@ -0,0 +1,111 @@ |
|||
<script> |
|||
import { automationStore } from "builderStore" |
|||
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui" |
|||
import { externalActions } from "./ExternalActions" |
|||
|
|||
export let block |
|||
export let blockComplete |
|||
export let showTestStatus = false |
|||
export let showParameters = {} |
|||
|
|||
$: testResult = |
|||
$automationStore.selectedAutomation?.testResults?.steps.filter(step => |
|||
block.id ? step.id === block.id : step.stepId === block.stepId |
|||
) |
|||
$: isTrigger = block.type === "TRIGGER" |
|||
|
|||
async function onSelect(block) { |
|||
await automationStore.update(state => { |
|||
state.selectedBlock = block |
|||
return state |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<div class="blockSection"> |
|||
<div |
|||
on:click={() => { |
|||
blockComplete = !blockComplete |
|||
showParameters[block.id] = blockComplete |
|||
}} |
|||
class="splitHeader" |
|||
> |
|||
<div class="center-items"> |
|||
{#if externalActions[block.stepId]} |
|||
<img |
|||
alt={externalActions[block.stepId].name} |
|||
width="28px" |
|||
height="28px" |
|||
src={externalActions[block.stepId].icon} |
|||
/> |
|||
{:else} |
|||
<svg |
|||
width="28px" |
|||
height="28px" |
|||
class="spectrum-Icon" |
|||
style="color:grey;" |
|||
focusable="false" |
|||
> |
|||
<use xlink:href="#spectrum-icon-18-{block.icon}" /> |
|||
</svg> |
|||
{/if} |
|||
<div class="iconAlign"> |
|||
{#if isTrigger} |
|||
<Body size="XS">When this happens:</Body> |
|||
{:else} |
|||
<Body size="XS">Do this:</Body> |
|||
{/if} |
|||
|
|||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail> |
|||
</div> |
|||
</div> |
|||
<div class="blockTitle"> |
|||
{#if showTestStatus && testResult && testResult[0]} |
|||
<div style="float: right;"> |
|||
<StatusLight |
|||
positive={isTrigger || testResult[0].outputs?.success} |
|||
negative={!testResult[0].outputs?.success} |
|||
><Body size="XS" |
|||
>{testResult[0].outputs?.success || isTrigger |
|||
? "Success" |
|||
: "Error"}</Body |
|||
></StatusLight |
|||
> |
|||
</div> |
|||
{/if} |
|||
<div |
|||
style="margin-left: 10px; margin-bottom: var(--spacing-xs);" |
|||
on:click={() => { |
|||
onSelect(block) |
|||
}} |
|||
> |
|||
<Icon name={blockComplete ? "ChevronUp" : "ChevronDown"} /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.center-items { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.splitHeader { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.iconAlign { |
|||
padding: 0 0 0 var(--spacing-m); |
|||
display: inline-block; |
|||
} |
|||
|
|||
.blockSection { |
|||
padding: var(--spacing-xl); |
|||
} |
|||
|
|||
.blockTitle { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
@ -1,133 +0,0 @@ |
|||
<script> |
|||
import { ModalContent, Icon, Detail, TextArea, Label } from "@budibase/bbui" |
|||
|
|||
export let testResult |
|||
export let isTrigger |
|||
let inputToggled |
|||
let outputToggled |
|||
</script> |
|||
|
|||
<ModalContent |
|||
showCloseIcon={false} |
|||
showConfirmButton={false} |
|||
cancelText="Close" |
|||
> |
|||
<div slot="header" class="result-modal-header"> |
|||
<span>Test Results</span> |
|||
<div> |
|||
{#if isTrigger || testResult[0].outputs.success} |
|||
<div class="iconSuccess"> |
|||
<Icon size="S" name="CheckmarkCircle" /> |
|||
</div> |
|||
{:else} |
|||
<div class="iconFailure"> |
|||
<Icon size="S" name="CloseCircle" /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
<span> |
|||
{#if testResult[0].outputs.iterations} |
|||
<div style="display: flex;"> |
|||
<Icon name="Reuse" /> |
|||
<div style="margin-left: 10px;"> |
|||
<Label> |
|||
This loop ran {testResult[0].outputs.iterations} times.</Label |
|||
> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
</span> |
|||
<div |
|||
on:click={() => { |
|||
inputToggled = !inputToggled |
|||
}} |
|||
class="toggle splitHeader" |
|||
> |
|||
<div> |
|||
<div style="display: flex; align-items: center;"> |
|||
<span style="padding-left: var(--spacing-s);"> |
|||
<Detail size="S">Input</Detail> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
{#if inputToggled} |
|||
<Icon size="M" name="ChevronDown" /> |
|||
{:else} |
|||
<Icon size="M" name="ChevronRight" /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{#if inputToggled} |
|||
<div class="text-area-container"> |
|||
<TextArea |
|||
disabled |
|||
value={JSON.stringify(testResult[0].inputs, null, 2)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
|
|||
<div |
|||
on:click={() => { |
|||
outputToggled = !outputToggled |
|||
}} |
|||
class="toggle splitHeader" |
|||
> |
|||
<div> |
|||
<div style="display: flex; align-items: center;"> |
|||
<span style="padding-left: var(--spacing-s);"> |
|||
<Detail size="S">Output</Detail> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
{#if outputToggled} |
|||
<Icon size="M" name="ChevronDown" /> |
|||
{:else} |
|||
<Icon size="M" name="ChevronRight" /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{#if outputToggled} |
|||
<div class="text-area-container"> |
|||
<TextArea |
|||
disabled |
|||
value={JSON.stringify(testResult[0].outputs, null, 2)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.result-modal-header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
width: 100%; |
|||
} |
|||
|
|||
.iconSuccess { |
|||
color: var(--spectrum-global-color-green-600); |
|||
} |
|||
|
|||
.iconFailure { |
|||
color: var(--spectrum-global-color-red-600); |
|||
} |
|||
|
|||
.splitHeader { |
|||
cursor: pointer; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.toggle { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.text-area-container :global(textarea) { |
|||
height: 150px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,146 @@ |
|||
<script> |
|||
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui" |
|||
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte" |
|||
import { automationStore } from "builderStore" |
|||
|
|||
export let automation |
|||
|
|||
let showParameters |
|||
let blocks |
|||
|
|||
$: { |
|||
blocks = [] |
|||
if (automation) { |
|||
if (automation.definition.trigger) { |
|||
blocks.push(automation.definition.trigger) |
|||
} |
|||
blocks = blocks |
|||
.concat(automation.definition.steps || []) |
|||
.filter(x => x.stepId !== "LOOP") |
|||
} |
|||
} |
|||
|
|||
$: testResults = |
|||
$automationStore.selectedAutomation?.testResults?.steps.filter( |
|||
x => x.stepId !== "LOOP" || [] |
|||
) |
|||
</script> |
|||
|
|||
<div class="title"> |
|||
<div class="title-text"> |
|||
<Icon name="MultipleCheck" /> |
|||
<div style="padding-left: var(--spacing-l)">Test Details</div> |
|||
</div> |
|||
<div style="padding-right: var(--spacing-xl)"> |
|||
<Icon |
|||
on:click={async () => { |
|||
$automationStore.selectedAutomation.automation.showTestPanel = false |
|||
}} |
|||
hoverable |
|||
name="Close" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<Divider /> |
|||
|
|||
<div class="container"> |
|||
{#each blocks as block, idx} |
|||
<div class="block"> |
|||
{#if block.stepId !== "LOOP"} |
|||
<FlowItemHeader showTestStatus={true} bind:showParameters {block} /> |
|||
{#if showParameters && showParameters[block.id]} |
|||
<Divider noMargin /> |
|||
{#if testResults?.[idx]?.outputs.iterations} |
|||
<div style="display: flex; padding: 10px 10px 0px 12px;"> |
|||
<Icon name="Reuse" /> |
|||
<div style="margin-left: 10px;"> |
|||
<Label> |
|||
This loop ran {testResults?.[idx]?.outputs.iterations} times.</Label |
|||
> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
|
|||
<div class="tabs"> |
|||
<Tabs quiet noPadding selected="Input"> |
|||
<Tab title="Input"> |
|||
<div style="padding: 10px 10px 10px 10px;"> |
|||
<TextArea |
|||
minHeight="80px" |
|||
disabled |
|||
value={JSON.stringify(testResults?.[idx]?.inputs, null, 2)} |
|||
/> |
|||
</div></Tab |
|||
> |
|||
<Tab title="Output"> |
|||
<div style="padding: 10px 10px 10px 10px;"> |
|||
<TextArea |
|||
minHeight="100px" |
|||
disabled |
|||
value={JSON.stringify(testResults?.[idx]?.outputs, null, 2)} |
|||
/> |
|||
</div> |
|||
</Tab> |
|||
</Tabs> |
|||
</div> |
|||
{/if} |
|||
{/if} |
|||
</div> |
|||
{#if blocks.length - 1 !== idx} |
|||
<div class="separator" /> |
|||
{/if} |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
padding: 0px 30px 0px 30px; |
|||
} |
|||
.title { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
gap: var(--spacing-xs); |
|||
padding-left: var(--spacing-xl); |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.tabs { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
position: relative; |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.title-text { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
} |
|||
|
|||
.title :global(h1) { |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.block { |
|||
display: inline-block; |
|||
width: 400px; |
|||
font-size: 16px; |
|||
background-color: var(--background); |
|||
border: 1px solid var(--spectrum-global-color-gray-300); |
|||
border-radius: 4px 4px 4px 4px; |
|||
} |
|||
|
|||
.separator { |
|||
width: 1px; |
|||
height: 40px; |
|||
border-left: 1px dashed var(--grey-4); |
|||
color: var(--grey-4); |
|||
/* center horizontally */ |
|||
text-align: center; |
|||
margin-left: 50%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,144 @@ |
|||
<script> |
|||
import { |
|||
Button, |
|||
ButtonGroup, |
|||
ModalContent, |
|||
Modal, |
|||
notifications, |
|||
ProgressCircle, |
|||
} from "@budibase/bbui" |
|||
import { auth, apps } from "stores/portal" |
|||
import { processStringSync } from "@budibase/string-templates" |
|||
import { API } from "api" |
|||
|
|||
export let app |
|||
export let buttonSize = "M" |
|||
|
|||
let APP_DEV_LOCK_SECONDS = 600 //common area for this? |
|||
let appLockModal |
|||
let processing = false |
|||
|
|||
$: lockedBy = app?.lockedBy |
|||
$: lockedByYou = $auth.user.email === lockedBy?.email |
|||
|
|||
$: lockIdentifer = `${ |
|||
lockedBy && lockedBy.firstName ? lockedBy?.firstName : lockedBy?.email |
|||
}` |
|||
|
|||
$: lockedByHeading = |
|||
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}` |
|||
|
|||
const getExpiryDuration = app => { |
|||
if (!app?.lockedBy?.lockedAt) { |
|||
return -1 |
|||
} |
|||
let expiry = |
|||
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000 |
|||
return expiry - new Date().getTime() |
|||
} |
|||
|
|||
const releaseLock = async () => { |
|||
processing = true |
|||
if (app) { |
|||
try { |
|||
await API.releaseAppLock(app.devId) |
|||
await apps.load() |
|||
notifications.success("Lock released successfully") |
|||
} catch (err) { |
|||
notifications.error("Error releasing lock") |
|||
} |
|||
} else { |
|||
notifications.error("No application is selected") |
|||
} |
|||
processing = false |
|||
} |
|||
</script> |
|||
|
|||
<div class="lock-status"> |
|||
{#if lockedBy} |
|||
<Button |
|||
quiet |
|||
secondary |
|||
icon="LockClosed" |
|||
size={buttonSize} |
|||
on:click={() => { |
|||
appLockModal.show() |
|||
}} |
|||
> |
|||
<span class="lock-status-text"> |
|||
{lockedByHeading} |
|||
</span> |
|||
</Button> |
|||
{/if} |
|||
</div> |
|||
|
|||
<Modal bind:this={appLockModal}> |
|||
<ModalContent |
|||
title={lockedByHeading} |
|||
dataCy={"app-lock-modal"} |
|||
showConfirmButton={false} |
|||
showCancelButton={false} |
|||
> |
|||
<p> |
|||
Apps are locked to prevent work from being lost from overlapping changes |
|||
between your team. |
|||
</p> |
|||
|
|||
{#if lockedByYou && getExpiryDuration(app) > 0} |
|||
<span class="lock-expiry-body"> |
|||
{processStringSync( |
|||
"This lock will expire in {{ duration time 'millisecond' }} from now", |
|||
{ |
|||
time: getExpiryDuration(app), |
|||
} |
|||
)} |
|||
</span> |
|||
{/if} |
|||
<div class="lock-modal-actions"> |
|||
<ButtonGroup> |
|||
<Button |
|||
secondary |
|||
quiet={lockedBy && lockedByYou} |
|||
disabled={processing} |
|||
on:click={() => { |
|||
appLockModal.hide() |
|||
}} |
|||
> |
|||
<span class="cancel" |
|||
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span |
|||
> |
|||
</Button> |
|||
{#if lockedByYou} |
|||
<Button |
|||
secondary |
|||
disabled={processing} |
|||
on:click={() => { |
|||
releaseLock() |
|||
appLockModal.hide() |
|||
}} |
|||
> |
|||
{#if processing} |
|||
<ProgressCircle overBackground={true} size="S" /> |
|||
{:else} |
|||
<span class="unlock">Release Lock</span> |
|||
{/if} |
|||
</Button> |
|||
{/if} |
|||
</ButtonGroup> |
|||
</div> |
|||
</ModalContent> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.lock-modal-actions { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
margin-top: var(--spacing-l); |
|||
gap: var(--spacing-xl); |
|||
} |
|||
.lock-status { |
|||
display: flex; |
|||
gap: var(--spacing-s); |
|||
max-width: 175px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,55 @@ |
|||
<script> |
|||
import { Icon, Detail } from "@budibase/bbui" |
|||
|
|||
export let title = "" |
|||
export let actionIcon |
|||
export let action |
|||
export let dataCy |
|||
|
|||
$: actionDefined = typeof action === "function" |
|||
</script> |
|||
|
|||
<div class="dash-card" data-cy={dataCy}> |
|||
<div class="dash-card-header" class:active={actionDefined} on:click={action}> |
|||
<span class="dash-card-title"> |
|||
<Detail size="M">{title}</Detail> |
|||
</span> |
|||
<span class="dash-card-action"> |
|||
{#if actionDefined} |
|||
<Icon name={actionIcon || "ChevronRight"} /> |
|||
{/if} |
|||
</span> |
|||
</div> |
|||
<div class="dash-card-body"> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.dash-card { |
|||
background: var(--spectrum-alias-background-color-primary); |
|||
border-radius: var(--border-radius-s); |
|||
overflow: hidden; |
|||
min-height: 150px; |
|||
} |
|||
.dash-card-header { |
|||
padding: var(--spacing-xl) var(--spectrum-global-dimension-static-size-400); |
|||
border-bottom: 1px solid var(--spectrum-global-color-gray-300); |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
.dash-card-body { |
|||
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2); |
|||
} |
|||
.dash-card-title :global(.spectrum-Detail) { |
|||
color: var( |
|||
--spectrum-sidenav-heading-text-color, |
|||
var(--spectrum-global-color-gray-700) |
|||
); |
|||
display: inline-block; |
|||
} |
|||
.dash-card-header.active:hover { |
|||
background-color: var(--spectrum-global-color-gray-200); |
|||
cursor: pointer; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,47 @@ |
|||
<script> |
|||
import { Icon } from "@budibase/bbui" |
|||
import ChooseIconModal from "components/start/ChooseIconModal.svelte" |
|||
|
|||
export let name |
|||
export let size |
|||
export let app |
|||
|
|||
let iconModal |
|||
</script> |
|||
|
|||
<div class="editable-icon"> |
|||
<div |
|||
class="edit-hover" |
|||
on:click={() => { |
|||
iconModal.show() |
|||
}} |
|||
> |
|||
<Icon name={"Edit"} size={"L"} /> |
|||
</div> |
|||
<div class="app-icon"> |
|||
<Icon {name} {size} /> |
|||
</div> |
|||
</div> |
|||
<ChooseIconModal {app} bind:this={iconModal} /> |
|||
|
|||
<style> |
|||
.editable-icon:hover .app-icon { |
|||
opacity: 0; |
|||
} |
|||
.editable-icon { |
|||
position: relative; |
|||
} |
|||
.editable-icon:hover .edit-hover { |
|||
opacity: 1; |
|||
} |
|||
.edit-hover { |
|||
color: var(--spectrum-global-color-gray-600); |
|||
cursor: pointer; |
|||
z-index: 100; |
|||
width: 100%; |
|||
height: 100%; |
|||
position: absolute; |
|||
opacity: 0; |
|||
/* transition: opacity var(--spectrum-global-animation-duration-100) ease; */ |
|||
} |
|||
</style> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue