I try to build a string in html-format to send it as an E-Mail in a jenkins-pipeline. I have following bit of code:
import groovy.xml.MarkupBuilder
def writer = new StringWriter()
def html = new MarkupBuilder( writer )
html.table( class: "squishSummary" ) {
style( type: "text/css" ) {
mkp.yield( getTableStyle() )
}
...
}
def getTableStyle() {
....
}
Unfortunately this runs into an error while executing the pipeline: java.lang.NoSuchMethodError: No such DSL method 'style' found among steps [...] If I'm understanding it correctly the jenkins-server tries to execute the style-command as a jenkins build-step, what reasonably fails. How can I fix this?
the following code works fine:
import groovy.xml.MarkupBuilder
def writer = new StringWriter()
def html = new MarkupBuilder( writer )
html.table( class: "squishSummary" ) {
style( type: "text/css" ) {
mkp.yield("123")
}
}
println writer
however if you declare variable named style then you get the error similar to yours
so this code will fail with groovy.lang.MissingMethodException: No signature of method ...
import groovy.xml.MarkupBuilder
def writer = new StringWriter()
def html = new MarkupBuilder( writer )
def style = "my style1" //just add this line and code fails
html.table( class: "squishSummary" ) {
style( type: "text/css" ) {
mkp.yield("123")
}
}
println writer
to solve problem use delegate or GString instead of plain method name:
import groovy.xml.MarkupBuilder
def writer = new StringWriter()
def html = new MarkupBuilder( writer )
def style = "style1"
html.table( class: "squishSummary" ) {
delegate.style( type: "text/css" ) {
mkp.yield("123")
}
//or like this:
"${'style'}"( type: "text/css" ) {
mkp.yield("123")
}
}
println writer
Related
I'm trying to create a playwright test (in javascript) that uses the page object model of classes, but where the test and page object model aren't in the same directory path.
The problem I'm having is it can't find my page-object-model class file. The error is Error: Cannot find module './pom/home-page'. What am I missing or doing wrong?
My file setup and path structure are as follows:
/package.config.js
...
const config = {
testDir: './test/playwright',
...
/test/playwright/pom/home-page.js
const { expect } = require ('#playwright/test');
exports.HomePage = class HomePage {
constructor(page) {
this.page = page;
this.searchInput = page.locator('#searchInput');
this.searchButton = page.locator('#searchButton');
}
}
/test/playwright/scripts/home/search.spec.js
const {test, expect} = require('#playwright/test');
const {HomePage} = require('./pom/home-page');
test.beforeAll( async ({ page }) => { ... });
test.beforeEach( async ({ page }) => { ... });
test.afterAll( async ({ page }) => { ... });
test.describe( 'As a user I want to search', () => {
test('"mySearchTerm1" and return {the expected result}', async ({ page }) => {
const homePage = new HomePage(page);
...
});
test('"mySearchTerm2" and return {the expected result}', async ({ page }) => {
const homePage = new HomePage(page);
...
});
});
Those using TypeScript can simplify this using tsconfig.json
https://playwright.dev/docs/test-typescript#manually-compile-tests-with-typescript
in tsconfig add:
"baseUrl": ".",
"paths":{
"#pages/*":[
"/test/playwright/pom/*"
]
}
Then you can import it in your fixture or test file like this:
import { HomePage } from "#pages/home-page"
This can be used to shorten fixtures or other files.
So, apparently the file reference is relative to the directory the test is located, not the testDir directory defined in the config file. I need to change line 2 in search.spec.js
const {HomePage} = require('../../pom/home-page');
Several problems. To begin with I get the following error in my devtools
WebSocket connection to 'ws://localhost:3000/cable?token=ZmllcnlAc3dhZ2dlci5jb20=' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED
On my rails server, I have the following output in a doc file on the project's repo: DOWNLOAD
It was rather too large for a blockquote.
To make a long story short, the connection is timing out half the time (resetting the server fixes this). However, most of the time it seems like the subscription silently fails to fire, as even making a new article from a different browser won't update the index page. I am completely lost as to what could be causing this to fail.
Relevant code:
Rails Side ApplicationCable
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = current_user
end
private
def current_user
token = request.params[:token].to_s
email = Base64.decode64(token)
User.find_by(email: email)
end
end
end
GraphQL Channel
class GraphqlChannel < ApplicationCable::Channel
def subscribed
#subscription_ids = []
end
def execute(data)
result = execute_query(data)
payload = {
result: result.subscription? ? { data: nil } : result.to_h,
more: result.subscription?
}
#subscription_ids << context[:subscription_id] if result.context[:subscription_id]
transmit(payload)
end
def unsubscribed
#subscription_ids.each do |sid|
SwyleSchema.subscriptions.delete_subscription(sid)
end
end
private
def execute_query(data)
SwyleSchema.execute(
query: data["query"],
context: context,
variables: data["variables"],
operation_name: data["operationName"]
)
end
def context
{
current_user_id: current_user.id,
current_user: current_user,
channel: self
}
end
end
Connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = current_user
end
private
def current_user
token = request.params[:token].to_s
email = Base64.decode64(token)
User.find_by(email: email)
end
end
end
Subscription Type
module Types
class SubscriptionType < GraphQL::Schema::Object
field :article_added, Types::ArticleType, null: false, description: "An article was posted"
def article_added
end
end
end
Create Article Mutation
module Mutations
class CreateArticle < BaseMutation
argument :title, String, required: true
argument :body, String, required: true
type Types::ArticleType
def resolve(title: nil, body: nil)
snippet = body[0, 300]
article = Article.new
article.title = title
article.body = body
article.snippet = snippet
article.user = context[:current_user]
if article.save
SwyleSchema.subscriptions.trigger("articleAdded", {}, article)
# { article: article}
article
else
{ errors: article.errors.full_messages }
end
end
end
end
Apollo.js
import { HttpLink } from 'apollo-link-http';
import { ApolloLink, Observable } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { onError } from 'apollo-link-error';
import { ApolloClient } from 'apollo-client';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import introspectionQueryResultData from './fragmentTypes.json';
import ActionCable from 'actioncable';
import ActionCableLink from 'graphql-ruby-client/subscriptions/ActionCableLink';
const getCableUrl = () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.hostname;
const port = process.env.CABLE_PORT || '3000';
const authToken = localStorage.getItem('mlToken');
debugger;
return `${protocol}//${host}:${port}/cable?token=${authToken}`;
};
const createActionCableLink = () => {
const cable = ActionCable.createConsumer(getCableUrl());
return new ActionCableLink({ cable });
};
const hasSubscriptionOperation = ({ query: { definitions } }) =>
definitions.some(
({ kind, operation }) =>
kind === 'OperationDefinition' && operation === 'subscription'
);
const getTokens = async () => {
const tokens = {
"X-CSRF-Token": document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content")
};
const authToken = await localStorage.getItem("mlToken");
return authToken ? { ...tokens, Authorization: authToken } : tokens;
};
const setTokenForOperation = async operation => {
return operation.setContext({
headers: {
// eslint-disable-next-line
... await getTokens(),
}
});
};
const createLinkWithToken = () =>
new ApolloLink(
(operation, forward) =>
new Observable(observer => {
let handle;
Promise.resolve(operation)
.then(setTokenForOperation)
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
const createHttpLink = () => new HttpLink({
uri: 'http://localhost:3000/graphql',
credentials: 'include',
})
const logError = (error) => console.error(error);
const createErrorLink = () => onError(({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
logError('GraphQL - Error', {
errors: graphQLErrors,
operationName: operation.operationName,
variables: operation.variables,
});
}
if (networkError) {
logError('GraphQL - NetworkError', networkError);
}
})
export const createClient = (cache, requestLink) => {
const client = new ApolloClient({
link: ApolloLink.from([
createErrorLink(),
createLinkWithToken(),
ApolloLink.split(
hasSubscriptionOperation,
createActionCableLink(),
createHttpLink(),
)
// createHttpLink(),
]),
cache,
});
return client;
};
export const createCache = () => {
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData
});
const cache = new InMemoryCache({fragmentMatcher});
if (process.env.NODE_ENV === 'development') {
window.secretVariableToStoreCache = cache;
}
return cache;
};
ARticles Index Component
import React, {Component} from 'react';
import articles from './queries/articles';
import { Query } from "react-apollo";
import {Link} from 'react-router-dom';
import ArticleTags from './article_tags';
import Subscription from './subscription';
class ArticlesIndex extends Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
const date = Date.now();
return (
<Query query={articles}>
{({ loading, error, data, subscribeToMore }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
const articles = data.articles;
return (
<div className="article-index-page">
<h1>Newest Articles</h1>
{ articles.map((article) => (
<div className="article-index-card" key={`${article.id}${article.title}${date}`}>
<h2 className="article-index-title">{article.title}</h2>
<h3 className="article-index-subtitle">by {article.author.username}</h3>
<p className="article-index-snippet">{article.snippet}<Link className="article-index-show-link" to={`/articles/${article.id}`}>{"...more"}</Link></p>
<ArticleTags tags={["lookAtThisTag", "othertag"] } />
<h4>{article.count} Comments {article.likeCount} Likes</h4>
</div>
))}
<Subscription subscribeToMore={subscribeToMore} />
</div>
)
}}
</Query>
);
}
}
export default ArticlesIndex;
Subscription Component (as per evilmartians)
import React, { useEffect } from 'react';
import ArticleSubscription from './subscriptions/article_added';
import { graphql } from 'react-apollo';
const Subscription = ({ subscribeToMore }) => {
useEffect(() => {
return subscribeToMore({
document: ArticleSubscription,
updateQuery: (prev, { subscriptionData }) => {
console.log("subdata: ", subscriptionData.data)
if (!subscriptionData.data) return prev;
const { articleAdded } = subscriptionData.data;
if (articleAdded) {
const alreadyInList = prev.articles.find(e => e.id === articleAdded.id);
if (alreadyInList) {
return prev;
}
return { ...prev, articles: prev.articles.concat([articleAdded]) };
}
return prev;
},
});
}, []);
return null;
};
export default Subscription;
And the subscriptions themselves
import gql from 'graphql-tag';
const ArticleSubscription = gql`
subscription ArticleSubscription {
articleAdded {
id
title
body
likers
likeCount
author {
id
username
},
currentUser {
id
username
email
}
}
}`
export default ArticleSubscription;
I'm having trouble understanding how a local file path from a smartphone could possibly get uploaded on the server side with a Rails api for instance.
The file path that we're sending to the backend doesn't mean anything to the server?
I'm getting a uri from the response like this:
file:///Users/.../Documents/images/5249F841-388B-478D-A0CB-2E1BF5511DA5.jpg):
I have tried to send something like this to the server:
let apiUrl = 'https://vnjldf.ngrok.io/api/update_photo'
let uriParts = uri.split('.');
let fileType = uri[uri.length - 1];
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileType}`,
type: `image/${fileType}`,
});
let options = {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
},
};
But I'm unsure what it is and how to decript it on the backend.
I have also tried sending the uri direclty but of course I'm getting the following error:
Errno::ENOENT (No such file or directory # rb_sysopen -...
Any help/guidance would be much appreciated.
I have recently spent 1+ hour debugging something similar.
I found out that if you make a POST to your Rails backend from your React Native app using this json:
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileName}`,
type: `image/${fileType}`,
});
Rails will automatically give you a ActionDispatch::Http::UploadedFile in your params[:photo], which you can attach directly to your model like Photo.create(photo: params[:photo]) and it simply works.
However, if you don't pass a filename, everything breaks and you'll get a huge string instead and it will raise a ArgumentError (invalid byte sequence in UTF-8).
So, based on your code, I can spot the bug right on: you are passing name as photo.${fileType}, which is wrong, and should be photo.${fileName} (update accordingly to get your image filename ... console.log(photo) in your React Native code will show you the correct one.
Maintain issues with deleting and adding new files
This is how I managed to do it add multiple file upload and maintain issues with deleting and adding new files
class User < ApplicationRecord
attribute :photos_urls # define it as an attribute so that seriallizer grabs it to generate JSON i.e. as_json method
has_many_attached :photos
def photos_urls
photos.map do |ip|
{url: Rails.application.routes.url_helpers.url_for(ip), signed_id: ip.signed_id}
end
end
See about signed_id here. It describes how you can handle multiple file upload.
Controller looks like
def update
user = User.find(params[:id])
if user.update(user_params)
render json: {
user: user.as_json(except: [:otp, :otp_expiry])
}, status: :ok
else
render json: { error: user.errors.full_messages.join(',') }, status: :bad_request
end
end
...
private
def user_params
params.permit(
:id, :name, :email, :username, :country, :address, :dob, :gender,
photos: []
)
end
React Native part
I am using react-native-image-crop-picker
import ImagePicker from 'react-native-image-crop-picker';
...
const photoHandler = index => {
ImagePicker.openPicker({
width: 300,
height: 400,
multiple: true,
}).then(selImages => {
if (selImages && selImages.length == 1) {
// Make sure, changes apply to that image-placeholder only which receives 'onPress' event
// Using 'index' to determine that
let output = images.slice();
output[index] = {
url: selImages[0].path, // For <Image> component's 'source' field
uri: selImages[0].path, // for FormData to upload
type: selImages[0].mime,
name: selImages[0].filename,
};
setImages(output);
} else {
setImages(
selImages.map(image => ({
url: image.path, // For <Image> component's 'source' field
uri: image.path, // for FormData to upload
type: image.mime,
name: image.filename,
})),
);
}
});
};
...
<View style={style.imageGroup}>
{images.map((item, index) => (
<TouchableOpacity
key={`img-${index}`}
style={style.imageWrapper}
onPress={() => photoHandler(index)}>
<Image style={style.tileImage} source={item} />
</TouchableOpacity>
))}
</View>
Uploader looks like
// ../models/api/index.js
// Update User
export const updateUser = async ({ id, data }) => {
// See https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
let formData = new FormData(data);
for (let key in data) {
if (Array.isArray(data[key])) {
// If it happens to be an Image field with multiple support
for (let image in data[key]) {
if (data[key][image]?.signed_id) {
// if the data has not change and it is as it was downloaded from server then
// it means you do not need to delete it
// For perverving it in DB you need to send `signed_id`
formData.append(`${key}[]`, data[key][image].signed_id);
} else if (data[key][image]?.uri && data[key][image]?.url) {
// if the data has change and it is as it has been replaced because user selected a different image in place
// it means you need to delete it and replace it with new one
// For deleting it in DB you should not send `signed_id`
formData.append(`${key}[]`, data[key][image]);
}
}
} else {
formData.append(key, data[key]);
}
}
return axios.patch(BASE_URL + "/users/" + data.id, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
};
and Saga worker looks like
import * as Api from "../models/api";
// worker Saga:
function* updateUserSaga({ payload }) {
console.log('updateUserSaga: payload', payload);
try {
const response = yield call(Api.updateUser, {
id: payload.id,
data: payload,
});
if (response.status == 200) {
yield put(userActions.updateUserSuccess(response.data));
RootNavigation.navigate('HomeScreen');
} else {
yield put(userActions.updateUserFailure({ error: response.data.error }));
}
} catch (e) {
console.error('Error: ', e);
yield put(
userActions.updateUserFailure({
error: "Network Error: Could not send OTP, Please try again.",
})
);
}
}
I'm trying to run this pipeline script
#!groovy
#NonCPS
def findCommand(filePath) {
def file = new File(filePath)
def text = file.getText()
def components = new XmlSlurper().parseText( text )
def map = new HashMap<>()
components.component.each { def component->
map << ["${component.#application}" : [:]]
def componentMap = map.get("${component.#application}")
component.environments.environment.each { def environment->
componentMap << ["${environment.#name}" : [:]]
def actionMap = componentMap.get("${environment.#name}")
environment.actions.action.each { def action->
actionMap << ["${action.#toDo}" : action]
}
}
}
components = null
text = null
file = null
return map
}
pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Building..'
echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
echo "Parameters: ${params.environment} ${params.actions} ${params.applications}"
script{
try {
map = findCommand("XXX.xml")
} catch (Exception e) {
echo "Catching the NonCPS Exception";
}
echo "${map}"
}
build job: "sample echo", parameters: [
[$class: 'StringParameterValue', name: 'application', value: "${params.application}"],
[$class: 'StringParameterValue', name: 'environment', value: "${params.environment}"]
]
}
}
}
post {
failure {
echo "Build failed..."
}
}
}
I know I'm using XmlSlurper, but I've also specified the code-block to be #NonCPS. However, this is still throwing a java.IO.NonSerializableException.
I also tried using a try-catch block to catch the exception, but that didn't seem to work either.
I'd gladly appreciate some help in how I could make the build completely cleanly. Thanks :(
The answer was in the exception. It's quite weird, but changing
components.component.each { def component->
map << ["${component.#application}" : [:]]
def componentMap = map.get("${component.#application}")
component.environments.environment.each { def environment->
componentMap << ["${environment.#name}" : [:]]
def actionMap = componentMap.get("${environment.#name}")
environment.actions.action.each { def action->
actionMap << ["${action.#toDo}" : action]
}
}
}
to
components.component.each { def component->
def application = "${component.#application}".toString()
map << [ (application) : [:]]
def componentMap = map.get(application)
component.environments.environment.each { def environment->
def name = "${environment.#name}".toString()
componentMap << [ (name) : [:]]
def actionMap = componentMap.get(name)
environment.actions.action.each { def action->
def toDo = "${action.#toDo}".toString()
def actionString = action.toString()
actionMap << [ (toDo) : (actionString)]
}
}
}
solved the issue. It had to do with the (non serializable) XMLSlurper data structure type still being carried into my HashMap.
This is driving me nuts. 2.4.4 My integration tests all pass. Upgrading to 2.5.5 and I get errors like this all over the place:
No signature of method: Project.addToMonitorings() is applicable for argument types: (Monitoring) values: [Monitoring : (unsaved)] Possible solutions: getMonitorings()
I cannot seem to track down how to update the integration tests to make them pass again.
Example (current) Test:
class MonitoringServiceSpec extends Specification {
def monitoringService
TestDataFactory f // factory that builds objects so we can use them in other tests
def setup() {
f = new TestDataFactory()
}
void "results can be limited"() {
given:
Project p = f.getProject()
p.save(flush: true, failOnError: true)
def params = new EcosListParams(new GrailsParameterMap ([offset: 0, max:1, sortColumn: 'id', order: 'asc'], null))
when:
p.addToMonitorings(f.getMonitoring(p)).save(flush: true, failOnError: true)
p.refresh()
def results = monitoringService.getProjectMonitorings(params, p.id)
then:
results.totalCount == 2
results.size() == 1
}
...
}
I get this error everywhere in my app that have one-to-many relationships. They worked perfectly fine in 2.4.4.
Here is what I had to do to get it to work. The getMonitoring method in the data factory already added the Project to the object. It must implicitly do an addTo
import groovy.sql.Sql;
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap
import spock.lang.*
class MonitoringServiceSpec extends Specification {
def f = new TestDataFactory()
def proj
def sql
def dataSource
def monitoringService
def setup() {
sql = new Sql(dataSource);
proj = f.getProject().save()
Monitoring mon1 = f.getMonitoring(proj).save()
Monitoring mon2 = f.getMonitoring(proj).save()
// don't need addTo, the getMonitoring method above implicitly adds it to the project
proj.save(flush: true, failOnError: true).refresh()
}
void "results can be limited"() {
given:
def params = new EcosListParams(new GrailsParameterMap ([offset: 0, max:1, sortColumn: 'id', order: 'asc'], null))
when:
def results = monitoringService.getProjectMonitorings(params, proj.id)
then:
results.totalCount == 2
results.size() == 1
}
void "results can be offset"() {
given:
def params1 = new EcosListParams(new GrailsParameterMap ([offset: 0, max:1, sortColumn: 'id', order: 'asc'], null))
def params2 = new EcosListParams(new GrailsParameterMap ([offset: 1, max:1, sortColumn: 'id', order: 'asc'], null))
when:
def results1 = monitoringService.getProjectMonitorings(params1, proj.id)
def results2 = monitoringService.getProjectMonitorings(params2, proj.id)
then:
results1.size() > 0
results1.id != results2
}
}
TestDataFactory
Monitoring getMonitoring(Project p) {
HabitatObjectiveSuccess hos = HabitatObjectiveSuccess.list(max: 1).get(0);
return new Monitoring(visitDate: new Date(), notes: 'notes', created: new Date(), createdBy: getPerson(),
lastUpdated: new Date(), lastUpdatedBy: getPerson(), maintActivitiesOccurring: 2,
maintActivitiesOccurrText: 'maintActivitiesOccurrText', landownerObjectivesMet: 1,
landownerObjectivesMetText: 'landownerObjectivesMetText', speciesObjectivesMet: 1,
speciesObjectivesMetText: 'speciesObjectivesMetText', habitatObjectiveSuccess: hos,
habitatObjectiveSuccessText: 'habitatObjectiveSuccessText', project: p
)
}